TypeScript 是 JavaScript 的超集,為 JavaScript 的生態增加了型別機制,並最終將程式碼編譯為純粹的 JavaScript 程式碼。型別機制很重要嗎?最近的一些專案經歷讓我覺得這真的很重要。當你陷在一箇中大型專案中時(Web 應用日趨成為常態),沒有型別約束、型別推斷,總有種牽一髮而動全身的危機和束縛。Immutable.js 和 Angular 2 都在使用 TypeScript 做開發,它們都是體量頗大的專案,所以我決定嘗試一下 Typescript。此外我們還可以嘗試 Facebook 的 Flow,比較一下兩者的優劣。Typescript 對 ES6 也有良好的支援,目前組內專案使用 Babel 編譯 ES6,這也就自然而然的把 TypeScirpt 和 Flow / babel-plugin-tcomb 放在了對立面,也許下一篇文章就是介紹 Flow 和 babel-plugin-tcomb。
What and Why
如果你想對 TypeScript 有更深入的認識,那麼推薦你閱讀 Stack Overflow 上的問答 What is TypeScript and why would I use it in place of JavaScript? ,這一節也是對這篇問答的一個簡述。
雖然 JavaScript 是 ECMAScript 規範的標準實現,但並不是所有的瀏覽器都支援最新的 ECAMScript 規範,這也就限制了開發者使用最新的 JavaScript / ECMAScript 特性。TypeScript 同樣支援最新的 ECMAScript 標準,並能將程式碼根據需求轉換為 ES 3 / 5 / 6,這也就意味著,開發者隨時可以使用最新的 ECMAScript 特性,比如 module / class / spread operator 等,而無需考慮相容性的問題。ECMAScript 所支援的型別機制非常豐富,包括:interface、enum、hybird type 等等。
與 TypeScript 相似的工具語言還有很多,它們主要分為兩個陣營,一個是類似 Babel 的陣營,以 JavaScript 的方式編寫程式碼,致力於為開發者提供最新的 ECMAScript 特性並將程式碼編譯為相容性的程式碼;另一個則是 Coffeescript、Clojure、Dart 等的陣營,它們的語法與 JavaScript 迥然不同,但最終會編譯為 JavaScript。TypeScript 在這兩者之間取得了一種平衡,它既為 JavaScript 增加了新特性,也保持了對 JavaScript 程式碼的相容,開發者幾乎可以直接將 .js
檔案重新命名為 .ts
檔案,就可以使用 TypeScript 的開發環境,這種做法一方面可以減少開發者的遷移成本,一方面也可以讓開發者快速上手 TypeScript。
JavaScript 是一門解釋型語言,變數的資料型別具有動態性,只有執行時才能確定變數的型別,這種後知後覺的認錯方法會讓開發者成為除錯大師,但無益於程式設計能力的提升,還會降低開發效率。TypeScript 的型別機制可以有效杜絕由變數型別引起的誤用問題,而且開發者可以控制對型別的監控程度,是嚴格限制變數型別還是寬鬆限制變數型別,都取決於開發者的開發需求。新增型別機制之後,副作用主要有兩個:增大了開發人員的學習曲線,增加了設定型別的開發時間。總體而言,這些付出相對於程式碼的健壯性和可維護性,都是值得的。
目前主流的 IDE 都為 TypeScript 的開發提供了良好的支援,比如 Visual Studio / VS Code、Atom、Sublime 和 WebStorm。TypeScript 與 IDE 的融合,便於開發者實時獲取型別資訊。舉例來說,通過程式碼補全功能可以獲取程式碼庫中其他函式的資訊;程式碼編譯完成後,相關資訊或錯誤資訊會直接反饋在 IDE 中……
在即將釋出的 TypeScript 2.0 版本中,將會有許多優秀的特性,比如對 null 和 undefined 的檢查。cannot read property `x` of undefined
和 undefined is not a function
在 JavaScript 中是非常常見的錯誤。在 TypeScript 2.0 中,通過使用 non-nullable
型別可以避免此類錯誤:let x : number = undefined
會讓編譯器提示錯誤,因為 undefined 並不是一個 number,通過 let x : number | undefined = undefined
或 let x : number? = undefined
可以讓 x 是一個 nullable(undefined 或 null) 的值。如果一個變數的型別是 nullable,那麼 TypeScript 編譯器就可以通過控制流和型別分析來判定對變數的使用是否安全:
let x : number?;
if (x !== undefined)
// this line will compile, because x is checked.
x += 1;
// this line will fail compilation, because x might be undefined.
x += 1;
TypeScript 編譯器既可以將 source map 資訊置於生成的 .js
檔案中,也可以建立獨立的 .map
檔案,便於開發者在程式碼執行階段設定斷點、審查變數。此外,TypeScript 還可以使用 decorator 攔截程式碼,為不同的模組系統生成模組載入程式碼,解析 JSX 等。
Usage
這一節介紹 TypeScirpt 的一些基礎特性,算是拋磚引玉,希望引起大家嘗試和使用 TypeScript 的興趣。首先,從最簡單的型別標註開始:
// 原始值
const isDone: boolean = false;
const amount: number = 6;
const address: string = `beijing`;
const greeting: string = `Hello World`;
// 陣列
const list: number[] = [1, 2, 3];
const list: Array<number> = [1, 2, 3];
// 元組
const name: [string, string] = [`Sean`, `Sun`];
// 列舉
enum Color {
Red,
Green,
Blue
};
const c: Color = Color.Green;
// 任意值:可以呼叫任意方法
let anyTypes: any = 4;
anyTypes = `any`;
anyTypes = false
// 空值
function doSomething (): void {
return undefined;
}
// 型別斷言
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
TypeScript 中的 Interface 可以看做是一個集合,這個集合是對物件、類等內部結構的約定:
// 定義介面 Coords
// 該介面包含 number 型別的 x,string 型別的 y
// 其中 y 是可選型別,即是否包含該屬性無所謂
interface Coords {
x: number;
y?: string;
};
// 定義函式 where
// 該函式接受一個 Coords 型別的引數 l
function where (l: Coords) {
// doSomething
}
const a = { x: 100 };
const b = { x: 100, y1: `abc` };
// a 擁有 number 型別的 x,可以傳遞給 where
where(a);
// b 擁有 number 型別的 x 和 string 型別的 y1,可以傳遞給 where
where(b);
// 下面這種呼叫方式將會報錯,雖然它和 where(b) 看起來是一致的
// 區別在於這裡傳遞的是一個物件字面量
// 物件字面量會被特殊對待並經過額外的屬性檢查
// 如果物件字面量中存在目標型別中未宣告的屬性,則丟擲錯誤
where({ x: 100, y1: `abc` });
// 最好的解決方式是為介面新增索引簽名
// 新增如下所示的索引簽名後,物件字面量可以有任意數量的屬性
// 只要屬性不是 x 和 y,其他屬性可以是 any 型別
interface Coords {
x: number;
y?: string;
[propName: string]: any
};
上面的程式碼演示了介面對物件的約束,此外,介面還常用於約束函式的行為:
// CheckType 包含一個呼叫簽名
// 該呼叫簽名宣告瞭 getType 函式需要接收一個 any 型別的引數,並最終返回一個 string 型別的結果
interface CheckType {
(data: any): string;
};
const getType: CheckType = (data: any) : string => {
return Object.prototype.toString.call(data);
}
getType(`abc`);
// => `[object String]`
與老牌強型別語言 C#、Java 相同的是,Interface 也可以用於約束類的行為:
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
class
除了 ES6 增加的 Class 用法,TypeScript 還增加了 C++、Java 中常見的 public / protected / private 限定符,限定變數或函式的使用範圍。TypeScript 使用的是結構性型別系統,只要兩種型別的成員型別相同,則認為這兩種型別是相容和一致的,但比較包含 private 和 protected 成員的型別時,只有他們是來自同一處的統一型別成員時才會被認為是相容的:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
animal = rhino;
// Error: Animal and Employee are not compatible
animal = employee;
抽象類是供其他類繼承的基類,與介面不同的是,抽象類可以包含成員方法的實現細節,但抽不可以包含抽象方法的實現細節:
abstract class Animal {
// 抽象方法
abstract makeSound(): void;
// 成員方法
move(): void {
console.log(`roaming the earch...`);
}
}
function
新增型別機制的 TypeScript 在函式上最可以秀的一塊就是函式過載了:
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(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 (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
let pickedCard2 = pickCard(15);
console.log("card: " + pickedCard1.card + " of " + pickedCard1.suit);
console.log("card: " + pickedCard2.card + " of " + pickedCard2.suit);
編譯器首先會嘗試匹配第一個函式過載的宣告,如果型別匹配成功就執行,否則繼續匹配其他的過載宣告,因此引數的針對性越強的函式過載,越要靠前宣告。
genrics
function identity<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}
let myIdentity: {<T>(arg: T[]): T[]} = identity;
上面的程式碼展示了泛型的基本用法,這裡的 <T>
稱為泛型變數,通過這個宣告,我們可以確定傳入的引數型別和返回的資料型別是一致的,一旦確定了傳入的引數型別,也就確定了返回的資料型別。myIdentity
使用了帶有呼叫簽名的物件字面量定義泛型函式,實際上可以結合介面,寫出更簡潔的泛型介面:
interface IdentityFn {
<T>(arg: T[]): T[];
};
let myIdentity: IdentityFn = identity;
如果同一個泛型變數在介面中被反覆使用,那麼可以在定義介面名的同時宣告泛型變數:
interface IdentityFn<T> {
(arg: T[]): T[];
};
function identity<T>(arg: T[]): T[] {
console.log(arg.length);
return arg;
}
let myIdentity: IdentityFn<string> = identity;
在泛型介面之外,還可以使用泛型類,兩者的形式非常類似:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
泛型也可以直接繼承介面約束自己的行為:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
type inference
TypeScript 主要有兩種型別推斷方式:Best Common Type 和 Contextual Type。我們先介紹 Best Common Type:
let x = [0, 1, null];
對於上面程式碼中的變數 x,如果要推斷出它的型別,就必須充分考慮 [0, 1, null]
的型別,所以這裡進行型別推斷的順序是從表示式的葉子到根的,也就是先推斷變數 x 的值都包含什麼型別,然後總結出 x 的型別,是一種從下往上的推斷過程。
TypeScript 的型別推論也可以按照從上往下的順序進行,這被稱為 Contextual Type:
window.onmousedown = function(mouseEvent) {
// Error: Property `button` does not exist ontype `MouseEvent`
console.log(mouseEvent.buton);
};
在上面的示例中,TypeScript 型別推斷機制會通過 window.onmousedown
函式的型別來推斷右側函式表示式的型別,繼而推斷出 mouseEvent
的型別,這種從上到下的推斷順序就是 Contextual Type 的特徵。
這裡只對 TypeScript 的特性做簡單的介紹,更詳細的資料請參考以下資料:
React and Webpack
在 TypeScript 中開發 React 時有以下幾點注意事項:
-
對 React 檔案使用
.tsx
的副檔名 -
在 tsconfig.json 中使用
compilerOptions.jsx: `react`
-
使用 typings 型別定義
interface Props {
foo: string;
}
class MyComponent extends React.Component<Props, {}> {
render() {
return <span>{this.props.foo}</span>
}
}
<MyComponent foo="bar" />; // 正確
TypeScript 的官方文件中對 React 的開發做了一個簡單的演示,主要包含以下幾個部分:
-
使用 tsconfig.json 作為 TypeScript 的編譯配置檔案
-
使用 webpack 作為構建工具,需要安裝 webpack、ts-loader 和 source-map-loader
-
使用 typings 作為程式碼提示工具
具體的搭建流程可以參考文件 React & Webpack,此外,我個人寫過一個 TypeScript & Webpack & React 開發的最小化模板可供各位參考,與之等同的 Babel & Webpack & React 版本。
如果檢視模板之後對
import * as React from `react`
的方式有所疑惑,請檢視 TypeScript 的負責人 Anders Hejlsberg 在 issue#2242 中的詳細解析。