本文首發於個人博客,轉載請註明出處:http://kohpoll.github.io/blog...
日常咱們編寫 TypeScript 時,主要會使用類型註解(給變量、函數等加上類型約束),這能夠加強代碼可讀性、避免低級 bug。實際上 TypeScript 的類型系統設計的很是強大,強大到能夠單獨做爲一門編程語言。本文是本身學習 TypeScript 類型編程的一個總結,但願對你有幫助。html
本文不會對 TypeScript 的基礎語法和使用進行說明,你能夠參考互聯網上提供的優秀資料:git
參考 SCIP 中對於編程語言的描述。一門編程語言應該提供如下機制:github
下面咱們將以這三個方面爲線索來探索 TypeScript 的類型編程。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 不行。app
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 }
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]
看起來是否是和編程語言裏面的函數很類似?這些「函數」的輸入(參數)是類型,通過「運算」後,輸出是「類型」。接着咱們來看看在「函數體」(也就是等號右邊)裏面除了一些基本操做外,還能夠作些其餘什麼騷操做。
將已有類型轉換爲一個新的類型,相似 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' }
條件類型能夠理解爲「三元運算」,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,能夠取得對象某個屬性值的類型 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...