當個標題黨,其實並不算太深入
目的
最近準備開始刷type-challenges
,因此先梳理一下ts型別相關知識點。
遺漏知識點總結
ts
的type符合圖靈完備。這意味著ts
型別包含迴圈、判斷等一系列基本操作。
型別集合
型別應當作集合來看,其中:
unknown
是全集,包含所有型別,是所有型別的父級- 之前在泛型中會寫
Comp<T extends unknown>
看來是有些多此一舉了...
- 之前在泛型中會寫
never
是空集,是所有型別的子集
ts的設計符合里氏替換原則,即子集可以替換所有父級,這意味著:
interface Parent {
id: string;
}
// 符合里氏替換原則,不會報錯
interface Child extends Parent {
id: "childIdA"|"childIdB"; // "childIdA"|"childIdB"是string的子集
key: number; // Parent中沒有key,該屬性是Parent的擴充
}
// 不符合里氏替換原則,報錯
interface ChildError extends Parent {
id: number; // number不是string的子集
}
type A = {a: string};
type B = {a: "A"|"a", b: number};
let a: A = {a: "xxx"};
let b: B = {a: "a", b: 2};
// 符合B是A的子集,該賦值符合里氏替換原則, 反之 b=a 會報錯
a = b;
將TS的type看作程式語言
既然ts
的type是圖靈完備的,那麼它自然可以完成一切計算,因此可以看作是一門語言
函式
在型別語境中,可將泛型看做是函式
type Fn<T> = T; // 看作 const Fn = val => val;
迴圈
經常可以在程式碼中看到如下寫法:
type R = "A" | "B"
type T = {
[k in R]: k
}
這裡[k in R]
可看做迴圈for(const k in R){}
藉助此特性,可以完成如下操作:
type MyPick<T, K extends keyof T> = {
[k in K]: T[k]
}
type A = MyPick<{a: string, b: number, c: boolean}, "a"|"c">
當然這種迴圈只針對特定型別("a"|"b"
這種),稍微複雜點的迴圈還是得透過遞迴實現,例子的話,後面用ts type
實現四則運算時會用到。
條件語句
type
支援三元運算子,這個沒什麼好說的。。
需要注意的是,type
裡沒有等號,但可以用extends代替等號
type Equal5<T> = T extends 5 ? true : false;
來點練習
// eq1: 實現`GetProp<Obj, K>`(獲取Obj[K]型別)
type GetProp<Obj, K> = K extends keyof Obj ? Obj[K] : undefined;
// eq2: 實現getName<User>
type GetName<User> = GetProp<User, "name">
extends
也常和infer
一起用於型別推斷。
例如
// 實現KeyOf
type KeyOf<T> = T extends {[k in infer U]: unknown} ? U : never;
// 實現ValueOf
type ValueOf<T> = T extends { [k in keyof T]: infer U } ? U : never;
// 實現Parameters和ReturnType
type FuncBase<F, C extends "params"|"return"> = F extends (...params: infer P) => infer R ? (C extends "params" ? P : R) : never;
type Params<F> = FuncBase<F, "params">;
type Return<F> = FuncBase<F, "return">;
賦值
賦值部分參看前面型別集合章節,賦值要遵循里氏替換原則。
條件語句章節提到了infer
,或許可以用infer
實現解構賦值。
// 需要實現類似js的const {name, ...extra} = user,求extra。(其實就是Omit方法)
type MyPick<Obj, T extends keyof Obj> = {[k in T]: Obj[k]};
type MyExclude<Obj, T extends keyof Obj> = keyof Obj extends T|(infer U extends keyof Obj) ? U : never;
type MyOmit<Obj, T extends keyof Obj> = MyPick<Obj, MyExclude<Obj, T>>;
// 實現陣列的解構賦值const [A, ...extra] = arr;
type GetArrBase<T extends unknown[], C extends "first"|"rest"> = T extends [infer First, ...infer Rest] ? (C extends "first"?First: Rest): never;
type GetFirst<T extends unknown[]> = GetArrBase<T, "first">;
type GetRest<T extends unknown[]> = GetArrBase<T, "rest">;
物件
type
的物件可以類比js
中的物件,使用方法如下,注意最後一個例子
type Obj = {
name: string;
age: 20;
}
Obj["name"] // string;
Obj.age // Error
Obj["age"] // 20
Obj['name'|'age'] // string | 20 , 這個特性很重要!!!
利用這個特性,可以完成如下功能
interface Test {
a: string;
b: number;
c: boolean;
}
// 之前不知道這個特性時,用infer也能達到同樣的效果,但實現不如這個直觀
type ValueOf<T> = T[keyof T];
type R = ValueOf<Test>;
陣列
ts
中的陣列分為Array
陣列和Tuple
元組。
Array
陣列是諸如string[]
的寫法,類似java
或其他語言的陣列。
Tuple
元組更像是js
中的陣列,寫法是[string, number, boolean]
這種。
(注:js
中不存在真正意義上的陣列,陣列是在記憶體上開闢連續空間,每個單元格所佔記憶體都一樣,在js
中,陣列寫成['a', 5555, {a: 1}]
都沒問題,顯然在實現上不是真正的開闢了連續記憶體空間,應該是用連結串列模擬的,為了解決連結串列本身查詢慢的問題,應該是採用了跳錶或者紅黑樹的方式組織的?)
陣列中需要注意的點如下:
type A = string[];
type B = [string, number, boolean];
// =========================分割線==========================
// 重點注意!!!
A[0]; // string
A[1]; // string
B[0]; // string
B[1]; // number;
B[0|2]; // string|boolean
// 注意以下寫法,為什麼可以這麼寫,因為number是所有數字的集合
A[number]; // string
B[number]; // string | number | boolean
A["length"]; // number
B["length"]; // 3
// ts陣列同樣可以像js陣列那樣展開
type Spread<T extends unknown[]> = [...T]
Spread<[1,2,3]> // [1,2,3]
根據以上特性,很容易實現以下練習:
// eq1: 實現 `ItemOf`方法(獲取陣列中項的型別)
type ItemOf<T extends unknown[]> = T[number];
// 之前不知道這個特性時,用infer實現的程式碼如下
type ItemOfByinfer<T> = T extends (infer N)[] ? N : never;
// eq2:實現`Append`方法
type Append<T extends unknown[], Ele> = [...T, Ele];
// eq3: 實現返回陣列length+1
// ts雖然無法實現加減運算,但可以透過模擬生成對應新型別,返回其屬性,從而模擬加減運算
type LengthAddOne<T extends unknown[]> = [unknown, ...T]["length"];
四則運算
運算加減依賴於元組長度,因此先定義一些基本方法,注意..由於是依賴元組長度,因此無法算負數和小數,只能算正整數...
(注:雖然無法計算負數和小數,但ts的type依舊是圖靈完備的,位運算也只是01的運算,負數和小數都是一堆01的定義,比如把10000看做0,且最後兩位是小數,那麼9999就是 -0.01)
// 返回Push後的陣列
type Append<T extends unknown[], E = unknown> = [...T, U];
// 同理,返回Pop後的陣列程式碼如下,暫時用不到
// type RemoveTop<T extends unknown[]> = T extends [...(infer U), unknown] ? U : never;
type Tuple<N extends number, Arr extends unknown[] = []> = Arr["length"] extends N ? Arr : Tuple<N, Append<Arr>>
有了這些基本方法,先實現加法和減法
type Add<A extends number, B extends number> = [...Tuple<A>, ...Tuple<B>]["length"];
type Subtract<A extends number, B extends number> = Tuple<A> extends [...Tuple<B>, ...(infer U)] ? U["length"] : never;
乘法的話,A*B
就是A
個B
相加,簡易版乘法如下,思路不難,但直接用Add和Subtract封裝,很多寫法都提示巢狀太深。。
注意,這裡用於統計和的引數S以元組表示,因為所有運算都是以元組為基準,S用數字表示會先轉元組再轉數字,來來回回開銷比較大。
type MultipleBase<A extends number, B extends number, S extends unknown[] = []> =
B extends 0
? S["length"]
: MultipleBase<A, Subtract<B, 1>, [...S, ...Tuple<A>]>;
乘法還有最佳化的空間,例如2*100
,直接用這個算的是100個2相加,時間複雜度不如100*2
,而計算這麼最佳化的前提是,實現BiggerThan
方法。
type BiggerThan<A extends number, B extends number> = Tuple<A> extends [...Tuple<B>, ...infer U] ? (U["length"] extends (never|0) ? false : true): false;
// 最佳化後的乘法如下
type Mutiple<A extends number, B extends number> = BiggerThan<A, B> extends true ? MultipleBase<A, B> : MultipleBase<B, A>;
有了BiggerThan
,除法也好說,例如a/b
,判定b*2、b*3...b*n
和A的大小就行。
同乘法的實現,用於統計的引數R為元組。。
type Divide<A extends number, B extends number, R extends unknown[] = []> = BiggerThan<B, A> extends true ? R["length"] : Divide<Subtract<A,B>, B, Append<R>>;
至此,四則運算實現完畢。