背景認識:
TypeScript 是微軟開發一款開源的編程語言,本質上是向 JavaScript 增加靜態類型系統。它是 JavaScript 的超集,所有現有的 JavaScript 都可以不加改變就在其中使用。它是為大型軟件開發而設計的,它最終編譯產生 JavaScript,所以可以運行在瀏覽器、Node.js 等等的運行時環境。
靜態類型系統是什么
增加靜態這個定語,是為了和運行時的類型檢查機制加以區分,強調靜態類型系統是在編譯時進行類型分析。
JavaScript 不是一個靜態編譯語言,不存在編譯這一步驟。但從程序推理工具的角度來看,JavaScript 的配套中還是有不少的,比如ESLint這個不完備的程序推理工具
靜態類型系統與 Lint 工具的關系
ESLint的定義:
Code linting is a type of static analysis that is frequently used to find problematic patterns or code that doesn’t adhere to certain style guidelines.
區別一
同樣強調Static Analysis,不過更強調Certain Style Guidelines,Lint 工具是一種團隊協作時的風格規范工具。
靜態類型類型分析和Lint 工具的區別在于Lint 工具沒有Classifying phrases according to the kinds of values they compute。
Lint 工具無法基于類型對程序進行靜態分析,但兩者都有基于CFG (控制流圖,Control Flow Graph)對程序進行分析的能力。比如 TypeScript 的控制流分析、ESLint的complexity(當你想寫個比較復雜的迭代算法時,這個規則就是個渣) 規則等。
TypeScript 和 JavaScript 的關系
和一些基于 JavaScript 的激進語言不同(比如 CoffeeScript),TypeScript 的語法設計首先考慮的就是兼容 JavaScript,或者說對 JavaScript 的語法做擴展。基本上是在 JavaScript 的基礎之上增加了一些類型標記語法,以實現靜態類型分析。把這些類型標注語法去掉之后,仍是一個標準的 JavaScript 語言。
TypeScript 同樣也在做一些新語法編譯到老語法的事情(就像 Babel 做的), 基本實現常用的EcmaScript Stage 1以上的語法特性。
類型系統的益處
靜態類型分析首要優點就是能盡早的發現邏輯錯誤,而不是上線之后才發現。比如我們在 JavaScript 中經常發生的問題,函數返回值含混。在開發過程中堅信一個函數返回字符串,但到了線上接受了真實數據卻返回了undefined。看似一個簡單錯誤,卻可能給公司造成數以萬計的損失。
看個例子。
// 通過分數獲取圖標
functiongetRankIcon(score){
if(score >=100) {
return'';
}elseif(score >=500) {
return'';
}elseif(score >=1500) {
return'';
}
}
consticon = getRankIcon(5);
consticonArray = icon.split();
執行
> node taste.js
TypeError: Cannot read property 'split' of undefined
相同的邏輯我們用tsc編譯一下(甚至不需要增加任何的類型標注)。直接靜態分析出來程序有一個undefined。
> tsc --strictNullChecks taste.ts
x.ts(11,19): error TS2532: Object is possibly 'undefined'.
另一個重要的用處是作為維護工具(重構輔助工具),假如我們有一個很通用的函數,在工程里用的到處都是,有一天我們要在這個函數最前面增加一個參數。TypeScript 中你只需要改那個函數就好了,然后再執行靜態類型分析,所有和這個函數參數不匹配的地方都會提示出來。但是,在 JavaScript 里,這個改動很有可能被忽略或者漏掉,打包也不會報錯,然后發布后線上就掛了……
類型系統的另一個優點是強化規范編程,TypeScript 提供了簡便的方式定義接口。這一點在大型軟件開發時尤為重要,一個系統模塊可以抽象的看做一個 TypeScript 定義的接口。
用帶清晰接口的模塊來結構化大型系統,這是一種更為抽象的設計形式。接口設計(討論)與最終實現方式無關,對接口思考得越抽象越有利。
換句話說就是讓設計脫離實現,最終體現出一種IDL(接口定義語言,Interface Define Language),讓程序設計回歸本質。
看個例子。
interface Avatar {
cdnUrl: string;// 用戶頭像在 CDN 上的地址
filePath: string;// 用戶頭像在對象存儲上的路徑
fileSize: number;// 文件大小
}
interface UserProfile {
cuid?: string;// 用戶識別 ID,可選
avatar?: Avatar;// 用戶形象,可選
name: string;// 用戶名,必選
gender: string;// 用戶性別,必選
age: number;// 用戶年齡,必選
}
interface UserModel {
createUser(profile: UserProfile): string;// 創建用戶
getUser(cuid: string): UserProfile;// 根據 cuid 獲取用戶
listFollowers(cuid: string): UserProfile[];// 獲取所有關注者
followByCuid(cuid: string, who: string): string;// 關注某人
}
那我實現上述Interface也只需如下進行。
classUserModelImplimplementsUserModel{
createUser(profile: UserProfile): string {
// do something
}
// 把 UserModel 定義的都實現
}
文檔
讀程序時類型標注也有用處,不止是說人在讀的時候。基于類型定義 IDE 可以對我們進行很多輔助,比如找到一個函數所有的使用,編寫代碼時對參數進行提示等等。
更重要的是這種文檔能力不像純人工維護的注釋一樣,稍不留神就忘了更新注釋,最后注釋和程序不一致。
更強大的是,可以自動根據類型標注產生文檔,甚至都不需要編寫注釋(詳細的人類語言描述還是要寫注釋的)。
首先安裝全局的typedoc命令。
> npm install -g typedoc
然后我們嘗試對上面抽象的Interface產生文檔。
> typedoc taste.ts --module commonjs --out doc
然后下面就是效果了。
編寫第一個 TypeScript 程序
這一節會介紹如何開始體驗 TypeScript,下一節開始會介紹一些有特點、有趣的例子。
安裝 TypeScript。
npm install -g typescript
初始化工作區。
mkdir learning-typescript
cd learning-typescript
新建第一個測試文件。
touch taste.ts
我們剛才已經新建了一個名為taste.ts的文件,對 TypeScript 的后綴名為ts,那我們寫點什么進去吧!
taste.ts
functionsay(text: string){
console.log(text);
}
say('hello!');
然后執行命令(tsc 是剛才 npm 裝的 typescript 中帶的)。
tsc taste.ts
然后我們得到一個編譯后的文件taste.js,內容如下。
functionsay(text){
console.log(text);
}
say('hello!');
可以看到,只是簡單去除了 text 后面的類型標注,然后我們用node執行taste.js。
node taste.js
// hello!
完美執行,讓我再改寫東西看看?
taste.ts
functionsay(text: string){
console.log(text);
}
say(969);
然后再執行tsc taste.ts,然后就類型檢查就報錯了。這就是 TypeScript 的主要功能 —— 靜態類型檢查。
> tsc taste.ts
taste.ts(4,5): error TS2345: Argument of type '969' is not assignable to parameter of type 'string'.
看一個 JavaScript 的例子。
functiongetDefaultValue(key, emphasis){
letret;
if(key ==='name') {
ret ='GuangWong';
}elseif(key==='gender') {
ret ='Man';
}elseif(key ==='age') {
ret =23;
}else{
thrownewError('Unkown key '+ info.type);
}
if(emphasis) {
ret = ret.toUpperCase();
}
returnret;
}
getDefaultValue('name');// GuangWong
getDefaultValue('gender',true)// MAN
getDefaultValue('age',true)// Error: toUpperCase is not a function
這是一個簡單的函數,第一個參數key用來獲得一個默認值。第二參數emphasis為了某些場景下要大寫強調,只需要傳入true即可自動將結果轉成大寫。
但是我不小心將age的值寫成了數字字面量,如果我調用getDefaultValue('age', true)就會在運行時報錯。這個有可能是軟件上線了之后才發生,直接導致業務不可用。
TypeScript 就能避免這類問題,我們只需要進行一個簡單的標注。
functiongetDefaultValue(key, emphasis?){
letret: string;
if(key ==='name') {
ret ='GuangWong';
}elseif(key ==='gender') {
ret ='Man';
}elseif(key ==='age') {
ret =23;
}else{
thrownewError('Unkown key '+ key);
}
if(emphasis) {
ret = ret.toUpperCase();
}
returnret;
}
getDefaultValue('name');// GuangWong
getDefaultValue('gender',true)// MAN
getDefaultValue('age',true)// Error: toUpperCase is not a function
在tsc編譯時,邏輯錯誤會自動報出來。媽媽再也不怕我的邏輯混亂了!
> tsc taste.ts
x.ts(8,5): error TS2322: Type '23' is not assignable to type 'string'.
JavaScript 的類型我們稱為鴨子類型。
當看到一只鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那么這只鳥就可以被稱為鴨子。
鴨子類型總是有點損的感覺,不如叫做面向接口編程。所以 JavaScript 就是一門面向接口編程的語言,TypeScript 中相對應的就是Interface。
接下來看個例子。
interface Profile {
name: string;
gender:'man'|'woman';
age: number;
height?: number;
}
functionprintProfile(profile: Profile){
console.log('name', profile.name);
console.log('gender', profile. gender);
console.log('age', profile.age);
if(profile.height) {
console.log('height', profile.height);
}
}
printProfile({name:'GuangWong', gender:'man', age:23});
使用tsc編譯一切完美,那我們嘗試下面的調用。
printProfile({name:'GuangWong', age:23});
使用tsc編譯,報錯了!說沒有傳屬性gender。不過height也沒傳怎么沒報錯呢?因為height?: number,其中的?表示這個是可選的。
> tsc taste.ts
x.ts(19,14): error TS2345: Argument of type '{ name: string; age: number; }' is not assignable to parameter of type 'Profile'.
Property 'gender' is missing in type '{ name: string; age: number; }'.
接下來我們試著傳個非number的height試試看。
printProfile({height:'190cm', name:'GuangWong', gender:'man', age:23});
使用tsc編譯,報錯了!string類型無法賦值給number類型。
> tsc taste.ts
x.ts(17,14): error TS2345: Argument of type '{ height: string; name: string; gender: "man"; age: number; }' is not assignable to parameter of type 'Profile'.
Types of property 'height' are incompatible.
Type 'string' is not assignable to type 'number'.
這也是Interface的應用,假設我們有這么一個Interface,是某個架構師寫的讓我來實現一種事物,比如榴蓮。
type Fell ='good'|'bad';
interface Eatable {
calorie: number;
looks(): Fell;
taste(): Fell;
flavour(): Fell;
}
我只需要簡單的實現Eatable即可,即implements Eatable。
classDurianimplementsEatable{
calorie =1000;
looks(): Fell {
return'good';
}
taste(): Fell {
return'good';
}
flavour(): Fell {
return'bad';
}
}
如果我刪掉flavour的實現,那就會報錯了!說我錯誤的實現了Eatable。
> tsc taste.ts
x.ts(8,7): error TS2420: Class 'Durian' incorrectly implements interface 'Eatable'.
Property 'flavour' is missing in type 'Durian'.
什么重載啊、多態啊、分派啊,在 JavaScript 里都是不存在的!那都是都是我們 Hacking 出來,Ugly!
TypeScript 對函數重載有一定的支持,不過因為 TypeScript 不擴展 JavaScript 的運行時機制,還是需要我們來處理根據宗量分派的問題(說白了就是運行時類型判斷)。
下面是 TypeScript 文檔中的一個例子。
letsuits = ["hearts","spades","clubs","diamonds"];
functionpickCard(x: {suit: string; card: number; }[]):number;
functionpickCard(x: number):{suit: string; card: number; };
functionpickCard(x):any{
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if(typeofx =="object") {
letpickedCard =Math.floor(Math.random() * x.length);
returnpickedCard;
}
// Otherwise just let them pick the card
elseif(typeofx =="number") {
letpickedSuit =Math.floor(x /13);
return{ suit: suits[pickedSuit], card: x %13};
}
}
letmyDeck = [{ suit:"diamonds", card:2}, { suit:"spades", card:10}, { suit:"hearts", card:4}];
letpickedCard1 = myDeck[pickCard(myDeck)];
alert("card: "+ pickedCard1.card +" of "+ pickedCard1.suit);
letpickedCard2 = pickCard(15);
alert("card: "+ pickedCard2.card +" of "+ pickedCard2.suit);
這樣至少在函數頭的描述上清晰多了,而且函數的各個分派函數的類型定義也可以明確的標記出來了。