TS 挑戰記錄 (一)#
題目記錄在這裡: https://github.com/type-challenges/type-challenges/blob/main/README.md
這個挑戰主要是讓挑戰者更好的了解 TS 的類型系統,通過 type
這個語法來實現 TS 中的各種功能,感覺還挺有意思的!
同時type
是一個圖靈完備的系統?也就是可以實現類似C++
template 的元編程?🤔有空可以好好研究下:
https://github.com/microsoft/TypeScript/issues/14833
Hello World 挑戰:
// expected to be string
type HelloWorld = any
// you should make this work
type test = Expect<Equal<HelloWorld, string>>
第一個示例主要是展示 type
的用法,答案:
type HelloWorld = string
既然這題題目本質上都是為了讓你理解 type 的用法,那先記錄下學習 type 的一些筆記,不然後面的題目真的看不懂了
type 的主要用法#
這裡先總結一下 type 的主要用法,防止後面的代碼看不懂(其實我就是先做題再反過去學 type 關鍵字的😅)
1. 基本類型別名#
type Word = string;
這個很好理解,自定義一個類型 Word
,用來替代 string
2. 聯合類型#
type StringOrNumber = string | number;
這裡開始複雜起來了,不過可以理解為一個 union
,通過 StringOrNumber
可以聲明 string 或者 number 類型的變量
注意這裡的類型也可以是字面量類型,如
type ABC = "A" | "B" | "C"
這就表明 ABC 類型的變量值只能是 "A","B","C"
這三者之一
3. 交叉類型#
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
如果要使用該交叉類型,則
let person: Person = {
name: 'Alice',
age: 30
};
4. typeof && keyof#
typeof
是獲取一個變量或值的類型,有點類似C++
的 type,但是用途更廣泛
比如對一個函數使用 typeof
操作符,可以獲得函數對應的類型,有點類似 C++
auto 自動推導
const foo = (arg1: string, arg2: number): void => {}
通過 typeof
來定義參數類型
const bar = (f: typeof foo) => {
f("Hello", 42);
}
keyof
則是獲取一個類型的所有鍵名,如下示例
type Person {
name: string;
age: number;
}
type K = keyof Person; // "name" | "age"
extends#
extends 主要用於判斷給定的泛型變量是否可以賦值給指定的類型
T extends U ? X : Y
當然,後續你會發現 extends 有很多神奇的用法 🤣
in#
TS 的 in 運算符除了常用法外,還可以用於遍歷類型,可以作為元編程,舉例如下:
type Keys = 'a' | 'b'
type MyMap<T> = {
[K in Keys]: T
}
Keys 代表表示可以是 'a'
或者 'b'
這兩種字面量類型
K in Keys 則可以遍歷這兩種類型
最後的 MyMap
結構你可以理解成這樣:
{
a: T, // 'a' 類型,其實key必須是a
b: T // 'b' 類型,其實key必須是b
}
infer#
infer 的作用就是可以放在一個地方,用來指定推斷出的類型(我反正這麼理解的
看一個簡單的示例:
這裡我們用 (...args: any) => infer R
這裡是一個函數的形式,想像 infer R
是一個整體,其實代表的是箭頭函數的返回值,那麼 infer R
自然就等於是返回值類型了
type ReturnType<T> = T extends (...args: any) => infer R ? R : any;
使用示例:可以看到 ReturnType
推斷除了 R 是 string
類型
type T = (a: number) => string
type R = ReturnType<T> // string
補充完這些知識之後,就可以開始做題了 😉
實現 MyPick#
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = MyPick<Todo, 'title' | 'completed'>
const todo: TodoPreview = {
title: 'Clean room',
completed: false,
}
通過 keyof
可以獲取類型的所有屬性組成的字面量聯合類型,然後通過 in 遍歷即可
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
實現 MyReadonly#
interface Todo {
title: string
description: string
}
const todo: MyReadonly<Todo> = {
title: "Hey",
description: "foobar"
}
todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
給屬性加上 readonly
即可
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
}
Tuple to Object#
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
這個想法倒是不難,但是沒想全,直接看答案了:
type TupleToObject<T extends readonly (keyof any)[]> = {
[K in T[number]]: K;
};
有一些關鍵部分要解釋下:
keyof any
其實就是所有能表述的枚舉類型,即string | number | symbol
,因為any
類型就是這三種 🤣readonly (keysof any)[]
表示所有只讀的(string | number | symbol)[]
,即混合類型的數組[K in T[number]]
這裡就是用 number 來表示數字字面量
這裡為什麼要用
readonly (keyof any)[]
?,而不是readonly any[]
因為最後我們的呢結果是一個 Object,而 JS 中 Object 的 key 只能是 string
, number
和 symbol
,如果這裡改成 T extends readonly any[]
, 那麼就可能導致 undefined, boolean, null
這樣的值成為 key 從而報錯
另外一個比較難理解的就是 T[number]
,這個看個示例就懂了
type Person = {
name: string;
age: number;
};
type PersonName = Person["name"]; // PersonName 將得到 "string" 類型
type PersonAge = Person["age"]; // PersonAge 將得到 "number" 類型
可以理解為通過 T[K]
的語法獲得對應的 value 類型
依次類推:
type ArrayType = string[];
type ElementType = ArrayType[number]; // ElementType 將得到 "string" 類型
type TupleType = [string, number, boolean];
type TupleFirstElementType = TupleType[0]; // TupleFirstElementType 將得到 "string" 類型
type TupleSecondElementType = TupleType[1]; // TupleSecondElementType 將得到 "number" 類型
那麼答案中的 T[number]
即表示數組中每個元素的類型,再通過 K in T[number]
即可獲得每一個類型
First of Array#
type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]
type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3
這題的思路因為要求數組第一個元素的類型,數組的元素類型可以是任意的(沒有必須作為 Object Key 的限制)
如果直接取第一個元素
type First<T extends any[]> = T[0]
但是這裡要判斷數組是否為空,有兩種判斷方法
最簡單的當然是:
type First<T extends any[]> = T extends [] ? never : T[0]
但是 TS 的類型,還可以當做值一樣去運算,比如我們計算 T['length']
也是可以的,因此
type First<T extends any[]> = T['length'] extends 0 ? never : T[0]
還有一種思路就是:把數組做分解,想不出來😓
type First<T extends any[]> = T extends [infer A, ...infer rest] ? A : never
Length of Tuple#
type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']
type teslaLength = Length<tesla> // expected 4
type spaceXLength = Length<spaceX> // expected 5
這個直接取 length
屬性即可,但也可以不readonly
的,猜測是怕影響到原有的數組刻意加的吧
type Length<T extends readonly any[]> = T['length']
Exclude#
type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
這個應該是最難理解的了,答案是這個:
type MyExclude<T, U> = T extends U ? never : T;
明明 extends
只是判斷 T 是否能被賦值為 U 類型,
但是實際這裡的執行過程為
'a' extends 'a' ? never : 'a'
,返回 never'b' extends 'a' ? never : 'b'
,返回'b'
'c' extends 'a' ? never : 'c'
,返回'c'
最後結果拼接,自然返回就是 'b' | 'c'
了
參考這篇
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types
Awaited#
這個也是很難理解的🫠
type ExampleType = Promise<string>
type Result = MyAwaited<ExampleType> // string
剛開始看不是很簡單嗎,我直接
type MyAwaited<T> = T extends Promise<infer U> ? U : never;
但是你有沒有考慮到
Promise
裡面可能嵌套了Promise
呢?
那就改進下:(對你沒看錯 type 居然還支持遞歸)
type MyAwaited<T extends Promise<any>> = T extends Promise<infer U>
? U extends Promise<any>
? MyAwaited<U>
: U
: never;
理論上這樣應該就可以了吧,但是我看了點讚最多的一個答案如下:
type Thenable<T> = {
then: (onfulfilled: (arg: T) => unknown) => unknown;
}
type MyAwaited<T extends Thenable<any> | Promise<any>> = T extends Promise<infer Inner>
? Inner extends Promise<any> ? MyAwaited<Inner> : Inner
: T extends Thenable<infer U> ? U : false
這就有點懵了,為什麼還要額外定一個 Thenable
類型,並且額外判斷這種情況呢?暫時也沒想明白🤔
如果不自定義 Thenable
類型,也可以直接用 PromiseLike
,後面有空再研究下
If#
type A = If<true, 'a', 'b'> // expected to be 'a'
type B = If<false, 'a', 'b'> // expected to be 'b'
這個就很簡單了,第一个值必須 extends boolean
,並且需要判斷是否能被賦值給 true
類型(注意直接寫 C ? T :F
是不行的 🤣
type If<C extends boolean, T, F> = C extends true ? T : F;
Concat#
type Result = Concat<[1], [2]> // expected to be [1, 2]
想不到吧,數組類型居然還支持 ...
運算符
type Concat<T extends any[], U extends any[]> = [...T, ...U]
Includes#
type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
這裡看上去是要寫一個循環,但實際根據 extends
,如果是判斷能否轉為一個聯合類型,則會展開然後依次判斷,即
type Includes<T extends readonly any[], U> = U extends T[number] ? true : false
可以看到 T[number]
實際上是如下:
'Kars' | 'Esidisi' | 'Wamuu' | 'Santana'
然後 U extends T[number]
就會逐個去判斷
Push#
type Result = Push<[1, 2], '3'> // [1, 2, '3']
這個也很簡單了,直接展開 T,並且加上 U 即可
type Push<T extends any[], U> = [...T, U]
Unshift#
type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
很簡單不細說了
type Unshift<T extends any[], U> = [U, ...T]
Parameters#
const foo = (arg1: string, arg2: number): void => {}
type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
這個也挺簡單的,注意理清關係就好了
type MyParameters<T extends (...args:any) => any> = T extends (...args:infer P) => any ? P : never;