由於 typescript 越來越復雜. 所以特意開多一個篇幅來記入一些比較難的, 和一些到了一定程度需要知道的基礎.
主要參考
https://basarat.gitbook.io/typescript/ 高級書
https://jkchao.github.io/typescript-book-chinese/ 高級書中文版
1. 名詞術語
Basic Annotation 是基本注解 let a : string
Inline Type Annotation 是內聯類型注解 let a : { name: string }
Union 是聯合類型 string | number
intersection 是交叉類型 string & number
Tuple 是 元組類型[string, number]
Nullish Coalescing 是 ??
Optional Chaining 是可選鏈 obj?.name
Rest parameter 是 call(...args : string[])
Spread operator 是 call(...['a', 'b'])
Conditional 是 T extends string ? number : never;
Type Guard 是 類型保護 arg is Foo
Destructuring 是解構 const { name } = { name: 'Derrick' }
Naked type 是 type Naked<T> = T extends ... (沒有被任何東西包裝)
NotNaked type 是 type NotNaked<T> = { o: T } extends ... // 在對象里面, 算被抱着了
Utility Types 是 Partial, Required, 等這些東西
Mapped types 是 { [P in TKeys] : any }
object literals 是 { name: string }
2. Number Enums as flags
flags 這個概念 c# 也有 (點這里和這里), 經常看到 enum 可以配合 | 來用, 比如 :
PropertyInfo[] attrs = obj.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); //獲取attrs
簡單理解就是把一個 enum 變成 類似 enum list
typescript 也允許我們做到這點. 點這里
首先定義 enum 的時候需要多寫一些號碼和符號進去進去 (這里用到的是二進制的移位方法, 我不太懂)
enum Abc { A = 1, B = 1 << 0, C = 1 << 1, D = 1 << 2, E = 1 << 3 }
然后呢,就可以這樣子去使用了.
let x: Abc = Abc.A | Abc.B; if(x & Abc.B) { console.log('has B'); } const hasB = !!(x & Abc.B); // true const hasC = !!(x & Abc.C); // false
書里有寫它的原理,和如果添加移除,但我目前沒有使用到,所以就不研究了.
2.1 擴展特定 enum 方法
通過 namespace
enum Abc { A } namespace Abc { export function print(): string { return 'dada'; } } Abc.print()
3. 理解 target 和 lib 點這里
target es6 的話, lib 就會有 promise 這些東西.
有些時候我們想分開來管理.
target 指的是 output 出來的 js 等級
比如 es5, 那么它就不會有 Promise, Set, Map 之類的.
lib 是我們開發環境用的. 我們當然希望什么都有咯.
所以一個比較合理的做法就是,
target es5 出來的 js 是 es5
lib dom, es6, 開發的時候我們用的 es6 的 js
然后通過 Polyfill 來補上就好了.
4. declare overload method 點這里
沒有 overload 的情況下可以這樣 declare, 或者用 interface 也可以
type LongHand = {
(a: number): number;
};
type ShortHand = (a: number) => number;
overload 情況下只可以用第一種
type LongHandAllowsOverloadDeclarations = {
(a: number): number;
(a: string): string;
};
5. 兼容性和變體 點這里
兼容性是指一個類型是否可以當另一個類型來使用 (有時候過多的限制會讓代碼很難寫,所以哪怕是強類型我們也經常會要強轉之類的, 所以不可以檔死了)
對象結構
type IsExtends<T, U> = T extends B ? true : false; type A = { age: number }; type B = { age: number, name: string }; // 屬性可以多, 不可以少 type result1 = IsExtends<B, A>; // true
函數參數
type Method = (age: number) => void; type Method2 = () => void; // 參數可以少, 不可以多 type result2 = IsExtends<Method2, Method>; // true
上面都是比較簡單的例子,只是數量上的問題
復雜的情況是參數類型,返回類型這些.
比如 2 個方法對比, 第 1 個參數類型是子類, 第 2 個是父類, 這個能兼容嗎 ?
說到這里可能就需要引入變體的概念了
首先講一下定義
A ≼ B
意味着 A
是 B
的子類型。
A → B
指的是以 A
為參數類型,以 B
為返回值類型的函數類型。
協變(Covariant):只在同一個方向;
逆變(Contravariant):只在相反的方向;
雙向協變(Bivariant):包括同一個方向和不同方向 (通常這個是不安全的,但有時候沒有辦法就比如強轉一樣)
不變(Invariant):如果類型不完全相同,則它們是不兼容的。
來一個函數類型的例子, 2個函數是否可兼容依據參數類型和返回類型
灰狗 ≼ 狗 ≼ 動物
有一個方法是 狗 → 狗
那么怎樣的方法是和它兼容的 ?
1. 灰狗 → 灰狗
2. 灰狗 → 動物
3. 動物 → 動物
4. 動物 → 灰狗 (正解)
參數類型默認情況下是雙向的 (開啟 strictFunctionTypes 之后就換成逆變),也就是說,只要是父類或者子類都 ok,返回類型是協變的,所以 a extends b 的話, a 的返回子類是 ok 的.
沒開啟 strictFunctionTypes 的時候
type methodParent = (p: Parent) => Parent; type methodChild = (c: Child) => Child; type result = IsExtends<methodChild, methodParent>; // true
開啟之后就不行了, 改成父類就 ok
type methodParent = (p: Parent) => Parent; type methodChild = (c: Parent) => Child; type result = IsExtends<methodChild, methodParent>; // true
為什么參數是逆變的, 而返回值是協變的呢 ?
繼承的概念就是我可以用 B 去替代 A. A 方法可以跑的地方,我全部換成 B 方法,它也要可以跑.
假設方法 A 的參數是狗 (要求抽象), 那么調用 A 方法的時候就有可能傳入狗或者灰狗 (要求抽象, 但也可以傳入具體).
如果 B 方法的參數是灰狗 (要求具體), 而我拿 B 替換了 A, 當調用的人傳入狗的時候就不 ok 了 (因為 A 只要求抽象,所以調用的人可能傳入抽象, 但是 B 內部卻需要具體, 所以直接翻車).
所以參數是逆變的. (參數需要更抽象才行)
假設返回值 A 是狗 (給予具體), 那么獲取返回值之后使用的就是具體屬性.
如果返回值 B 是動物 (給予更抽象), 那么就翻車了咯
所以返回值是協變的 (返回值要更具體)
協變就是說要求 A 類的地方可以用 A 或 A 的子類來替換 (不可以用 A 的父類), 更具體
逆變就是要求 A 類的地方可以用 A 或 A 的父類來替換 (不可以用 A 的子類). 更抽象
雙向就是上面 2 個都可以
不變就是 A 只能用 A 不可以是其它的
6. Conditional, Infer
refer : https://fettblog.eu/typescript-union-to-intersection/ (聯合類型轉交叉類型)
當 contional 遇到 union (Distributive Conditional Types)
如果是 naked 的話會被拆成多個來解析, not naked 的話就是把 T 傳過去用而已
type Naked<T> = T extends any ? { name: T } : never; type result = Naked<'a' | 'b'>; // { name: 'a' } | { name: 'b' } type NotNaked<T> = T[] extends any ? { name: T } : never; type result2 = NotNaked<'a' | 'b'>; // { name: 'a' | 'b' }
Infer 有個特色就是如果 infer 是用在參數類型上, 由於是逆變 position, 最終出來的結果會變成交叉類型.
如果不是逆變的話,出來的結果會是聯合類型
// 這是轉為聯合類型 type Foo<T> = T extends { a: infer U, b: infer U } ? U : never; type T10 = Foo<{ a: string, b: string }>; // string type T11 = Foo<{ a: string, b: number }>; // string | number // 這是轉為交叉類型 type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never; type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>; // string type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>; // string & number
這幾招通常用在轉換 union to array, union to intersection 等