本文首發於我的部落格,轉載請註明出處:http://kohpoll.github.io/blog...
平常我們編寫 TypeScript 時,主要會使用型別註解(給變數、函式等加上型別約束),這可以增強程式碼可讀性、避免低階 bug。實際上 TypeScript 的型別系統設計的非常強大,強大到可以單獨作為一門程式語言。本文是自己學習 TypeScript 型別程式設計的一個總結,希望對你有幫助。
開始之前
本文不會對 TypeScript 的基礎語法和使用進行說明,你可以參考網際網路上提供的優秀資料:
啟程
參考 SCIP 中對於程式語言的描述。一門程式語言應該提供以下機制:
- 基本表示式。用來表示語言所關心的最簡單的個體。
- 組合的方法。從簡單的個體出發構造複合的物件。
- 抽象的方法。能將複合物件封裝作為獨立單元去使用。
下面我們將以這三個方面為線索來探索 TypeScript 的型別程式設計。
基本表示式
我們首先來看看型別程式設計中,定義“變數”的方式:
// string、number、boolean 的值可以作為型別使用,稱為 literal type
type LiteralS = 'x';
type LiteralN = 9;
type LiteralB = true;
// 基礎型別
type S = string;
// 函式
type F = (flag: boolean) => void;
// 物件
type O = { x: number; y: number; };
// tuple
type T = [string, number];
這裡稍微補充下 interface 和 type 的區別。
最主要的區別就是 type 可以進行“型別程式設計”,interface 不行。
interface 能定義的型別比較侷限,就是 object/function/class/indexable:
// object
interface Point {
x: number;
y: number;
}
const p: Point = { x: 1, y: 2 };
// function
interface Add {
(a: number, b: number): number;
}
const add: Add = (x, y) => x + y;
// class
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick(): void;
}
const Clock: ClockConstructor = class C implements ClockInterface {
constructor(hour: number, minute: number) { return this; }
tick() {}
}
const c = new Clock(1, 2);
c.tick();
// indexable
interface StringArray {
[index: number]: string;
}
interface NumberObject {
[key: string]: number;
}
const s: StringArray = ['a', 'b'];
const o: NumberObject = { a: 1, b: 2 };
interface 可以被重新“開啟”,同名 interface 會自動聚合,非常適合做 polyfill。比如,我們想要在 window 上擴充套件一些原本不存在的屬性:
interface Window {
g_config: {
locale: 'zh_CN' | 'en_US';
};
}
組合的方法
有了基本表示式,我們來看組合的方法。
| 和 & 操作符
& 表示必須同時滿足多個契約,| 表示滿足任意一個契約即可。
type Size = 'large' | 'normal' | 'small';
// never 可以理解為 | 運算的“么元”,即:x | never = x
type T = 1 | 2 | never; // 1 | 2
type Animal = { name: string };
type Flyable = { fly(): void };
type FlyableAnimal = Animal & Flyable; // { name: string, fly(): void }
keyof 操作符
interface Sizes {
large: string;
normal: string;
small: string;
x: number;
}
// 獲取物件的屬性值
type Size = keyof Sizes; // 'large' | 'normal' | 'small' | 'x'
// 反向獲取物件屬性的型別
type SizeValue = Sizes[keyof Sizes]; // string | number
// keyof any 可以理解為能作為“索引”的型別
type K = keyof any; // string | number | symbol
抽象的方法
抽象的方法實際上指的就是“函式”。我們來看看型別程式設計中,“函式”該怎麼定義。
// “定義”
type Id<T> = T;
// “呼叫”
type A = Id<'a'>; // 'a'
// “引數”約束及預設值
type MakePair<T extends string, U extends number = 1> = [T, U];
type P1 = MakePair<'a', 2>; // ['a', 2]
type P2 = MakePair<'x'>; // ['x', 1]
看起來是不是和程式語言裡面的函式很相似?這些“函式”的輸入(引數)是型別,經過“運算”後,輸出是“型別”。接著我們來看看在“函式體”(也就是等號右邊)裡面除了一些基本操作外,還可以做些其他什麼騷操作。
“對映”操作(mapped)
將已有型別轉換為一個新的型別,類似 map。返回的新型別一般是物件。
type MakeRecord<T extends keyof any, V> = {
[k in T]: V
};
type R1 = MakeRecord<1, number>; // { 1: number }
type R2 = MakeRecord<'a' | 1, string>; // { a: string, 1: string }
type TupleToObject<T extends readonly any[]> = {
[k in T[number]]: k
};
type O = TupleToObject<['a', 'b', 'c']>; // { a: 'a', b: 'b', c: 'c' }
條件——extends
條件型別可以理解為“三元運算”,T extends U ? X : Y,extends 可以類比為“相等”。
// 只保留string
type OnlyString<T> = T extends string ? T : never;
type S = OnlyString<1 | 2 | true | 'a' | 'b'>; // 'a' | 'b'
// 這裡的計算過程大致是:
// 1 | 2 | true | 'a' | 'b' -> never | never | never | 'a' | 'b'
// 根據 x | never = x,最終得到 'a' | 'b'
// 獲得物件的函式型別的屬性key值
type FunctionPropertyNames<T> = {
[k in keyof T]: T[k] extends Function ? k : never
}[keyof T];
interface D {
id: number;
add(id: number): void;
remove(id: number): void;
}
type P = FunctionPropertyNames<D>; // 'add' | 'remove'
// 這裡的計算過程大致是:
// 將 interface 展開:
// {
// id: D['id'] extends Function ? 'id' : never, //-> false
// add: D['add'] extends Function ? 'add' : never, //-> true
// remove: D['remove'] extends Function ? 'remove' : never //-> true
// }['id' | 'add' | 'remove']
// 計算條件型別:
// {
// id: never,
// add: 'add',
// remove: 'remove'
// }['id' | 'add' | 'remove']
// 根據索引取值:
// never | 'add' | 'remove'
// 根據 never | x = x,最終得到:'add' | 'remove'
“析構“——infer
infer 可以理解為一種“放大鏡”機制,可以“捕獲”到被“嵌”在各種複雜結構裡的型別資訊。
// 物件 infer,可以取得物件某個屬性值的型別
type ObjectInfer<O> = O extends { x: infer T } ? T : never;
type T1 = ObjectInfer<{x: number}>; // number
// 陣列 infer,可以取得陣列元素的型別
type ArrayInfer<A> = A extends (infer U)[] ? U : never;
const arr = [1, 'a', true];
type T2 = ArrayInfer<typeof arr>; // number | string | boolean
// tuple infer
type TupleInfer<T> = T extends [infer A, ...(infer B)[]] ? [A, B] : never;
type T3 = TupleInfer<[string, number, boolean]>; // [string, number | boolean]
// 函式 infer,可以取得函式的引數和返回值型別
type FunctionInfer<F> = F extends (...args: infer A) => infer R ? [A, R] : never;
type T4 = FunctionInfer<(a: number, b: string) => boolean>; // [[a: number, b: string], boolean]
// 更多其他的 infer
type PInfer<P> = P extends Promise<infer G> ? G : never;
const p = new Promise<number>(() => {});
type T5 = PInfer<typeof p>; // number
可以發現上面的例子需要使用 infer,是因為我們在“定義”時不知道具體的型別,需要在“呼叫”時做“推斷”。infer 幫我們標註了待推斷的型別,最終計算出實際的型別。
巢狀&遞迴
在“函式體”中,我們其實可以再“呼叫函式“,形成一種巢狀和遞迴的機制。
// 取函式第一個引數的型別
type Params<F> = F extends (...args: infer A) => any ? A : never;
type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
type FirstParam = Head<Params<(name: string, age: number) => void>>; // string
// 遞迴定義
type List<T> = {
value: T,
next: List<T>
} | null;
尾聲
文章寫到這裡基本就結束了,這篇文章的內容可能在平常的開發中會比較少遇到,但是對於補全自己的 TypeScript 體系、開闊視野還是有所幫助的。如果想更多的來些實戰演練,推薦看看這個:https://github.com/type-chall...