深入學習typescript type型別

goblin_pitcher發表於2023-05-15

當個標題黨,其實並不算太深入

所在倉庫地址:https://github.com/goblin-pitcher/steel-wheel-run/blob/master/typescript/type-challenge-pre-content.md

目的

最近準備開始刷type-challenges,因此先梳理一下ts型別相關知識點。

遺漏知識點總結

ts的type符合圖靈完備。這意味著ts型別包含迴圈、判斷等一系列基本操作。

型別集合

型別應當作集合來看,其中:

  1. unknown是全集,包含所有型別,是所有型別的父級

    • 之前在泛型中會寫Comp<T extends unknown>看來是有些多此一舉了...
  2. 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就是AB相加,簡易版乘法如下,思路不難,但直接用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>>;

至此,四則運算實現完畢。

相關文章