前言
在剛接觸 TypeScript 時(shí),我僅僅是對(duì)變量,函數(shù)進(jìn)行類型標(biāo)注,主要也就用到了 type、interface,泛型等內(nèi)容;后來因?yàn)橐_發(fā)組件庫,于是打開官方文檔稍微“進(jìn)修”了一下,了解了一些工具類型例如 Pick、ReturnType,Exclude 等,以及 tsconfig 的一些編譯配置,總的來說也是淺嘗輒止。
直到最近開發(fā)地圖組件庫時(shí),產(chǎn)生了一些奇怪的需求,比如:已知有事件 ['click', 'touch', 'close'] ,如何根據(jù)這個(gè)數(shù)組生成一個(gè)類型,其屬性為 onClick、onTouch,onClose ,向同事請(qǐng)教后未果,于是決定深入學(xué)習(xí)下 TypeScript。經(jīng)過一番學(xué)習(xí),實(shí)現(xiàn)了一個(gè)版本如下:
type EventToHandler<A extends readonly string[] , H> = {
[K in A[number] as `on${Capitalize<K>}`]: H
}
const event = ['click', 'touch', 'close'] as const;
type EventMap = EventToHandler<typeof event, (e: any) => void>

下面,我將分享對(duì)學(xué)習(xí)內(nèi)容的總結(jié)~
一、操作符
keyof
The keyof operator takes an object type and produces a string or numeric literal union of its keys.
keyof 操作符接受一個(gè)對(duì)象類型,并產(chǎn)生一個(gè)字符串或其鍵的數(shù)字字面值聯(lián)合類型。參考:https://www.typescriptlang.org/docs/handbook/2/keyof-types.html
interface Object {
p: string
q: number
}
type Key = keyof Object // 'p' | 'q'
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish; // number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish; // string | number
typeof
JavaScript already has a typeof operator you can use in an expression context, TypeScript adds a typeof operator you can use in a type context to refer to the type of a variable or property.
JavaScript已經(jīng)有了一個(gè) typeof 操作符,你可以在表達(dá)式上下文中使用,TypeScript添加了一個(gè) typeof 操作符,你可以在類型上下文中使用它來引用變量或?qū)傩缘念愋汀?/p>參考:https://www.typescriptlang.org/docs/handbook/2/typeof-types.html
// Javascript
typeof null === 'object' // true
// TypeScript
type A = typeof null // any
type B = typeof '1' // '1'
const obj = {
p: 1,
q: '1'
}
type Object = typeof obj // { p: number, q: string }
typeof 這個(gè)關(guān)鍵字可以延伸一下:JavaScript 中 typeof 可以幫助 TypeScript 實(shí)現(xiàn)類型收緊。
除此之外 instanceof 以及 TypeScript 中的 is 也有相同的作用。類型收緊在函數(shù)重載中很有用~
declare function isString(str: unknown): str is string
declare function isNumber(str: unknown): str is number
function main(str: number): number
function main(str: string): string
function main(str) {
if (isString(str)) {
// (parameter) str: string
return String(str);
}
if (isNumber(str)) {
// (parameter) str: number
return Number(str);
}
throw new Error('unExpected param type');
}
in
抱歉在官方文檔上沒有找到 in 操作符相關(guān)的解釋,我只能從實(shí)踐的角度總結(jié)它的作用:TypeScript 中的 in 在對(duì)象映射操作上起著至關(guān)重要的作用~下文會(huì)介紹其具體作用。
infer
TypeScript 中的 infer 用于在泛型類型中推斷出其某個(gè)參數(shù)的類型。通常情況下,我們可以將泛型類型傳遞給一個(gè)具體類型來獲取它的類型,但有時(shí)候需要從泛型類型中推斷出某個(gè)輸入類型或輸出類型,這時(shí)候就可以使用 infer 來實(shí)現(xiàn)。
注意:infer 只能用在 extends 之后。
type MyAwaited<P extends Promise<unknown>> = P extends Promise<infer T> ? T : never;
type Test = MyAwaited<Promise<string>> // string
type RetrunType<T> = T extends (...args: any[]) => infer U ? U : never
二、類型基礎(chǔ)
2.1 類型
基本類型
基本類型,也可以理解為原子類型。包括 number、boolean、string、null、undefined、function、array、symbol 字面量(true,false,1,"a")等,它們無法再細(xì)分。
復(fù)合類型
復(fù)合類型可以分為三類:
-
union,指一個(gè)無序的、無重復(fù)元素的集合。 -
tuple,可簡單看做一個(gè)只讀數(shù)組的類型。 -
map,和JavaScript中的對(duì)象一樣,是一些沒有重復(fù)鍵的鍵值對(duì)。
type union = '1' | '2' | true | symbol
const tuple = [1, 2, 3] as const;
type Tuple = typeof tuple;
interface Map {
name: string
age: number
}
2.2 取值方式
union
TypeScript 官方?jīng)]有提供 union 的取值方式,這也直接導(dǎo)致了和 union 相關(guān)的類型變換變得比較復(fù)雜。
tuple
因?yàn)?tuple 是 readonly Array<any> 類型,所以 tuple 也可以像數(shù)組一樣使用數(shù)字進(jìn)行索引。
const tuple = [1, 2, 3, '1'] as const;
type Tuple = typeof tuple;
type T0 = Tuple[0] // 1
type T3 = Tuple[3] // '1'
type T4 = Tuple[4] //
type Union = Tuple[number] // 1 | 2 | 3 | '1'
map
map 取值和 JavaScript 中對(duì)象取值的方式一致
interface Object {
p: string
q: number
}
type A = Object['p'] // string
type B = Object[keyof Object] // string | number
2.3 遍歷方式
在 TypeScript 的類型系統(tǒng)中無法使用循環(huán)語句,所以我們只能用遞歸來實(shí)現(xiàn)遍歷,能參與邏輯判斷的操作符只有 extends 和 三元運(yùn)算符 ? ... : ...。
union
union 的遍歷最簡單,只需要用 extends 即可完成。
type Exclude<T, U> = T extends U ? never : T
type A = Exclude<'1' | '2', '2'> // '1'
tuple
元組遍歷主要通過 infer 和擴(kuò)展運(yùn)算符 ... 實(shí)現(xiàn),通過檢查 rest 參數(shù)是否為空數(shù)組來判斷是否遞歸到最后一項(xiàng)。
export type Join<
A extends readonly string[],
S extends string,
P extends string = ''
> = A extends readonly [infer F extends string, ...infer R extends readonly string[]]
? R extends [] // F tuple 的最后一個(gè)元素
? `${P}${F}`
: Join<R, S, `${P}${F}${S}`>
: P
declare function join<A extends readonly string[], S extends string>(array: A, s: S): Join<A, S>
const arr = ['hello', 'world'] as const
const str = join(arr, ' ') // 'hello world'
type Str = Join<typeof arr, ' '> // 'hello world'
字面量數(shù)組
字符串的遍歷方式和數(shù)組類似,也通過 infer 實(shí)現(xiàn),另外還需要模板字符串輔助。
export type Split<
S extends string,
P extends string,
A extends string[] = []
> = S extends `${infer F}${infer R}` ?
R extends '' // F 已經(jīng)是最后一個(gè)字符
? F extends P
? A
: [...A, F] // F 是一個(gè)非分隔符的字符
: F extends P // F 不是最后一個(gè)字符
? Split<R, P, A> // F 是分隔符,那么丟棄
: Split<R, P, [...A, F]> // F 不是分隔符,
: string[]
declare function split<S extends string, P extends string>(str: S, p: P): Split<S, P>
const arr = split('1,2,3', ',') // ["1", "2", "3"]
map
嚴(yán)格來講,遍歷對(duì)象不能稱之為“遍歷”,而是“映射”,因?yàn)橐粋€(gè) map 只能映射成另外一個(gè) map,而不能變成其他的類型~遍歷對(duì)象主要通過 in 和 keyof 操作符實(shí)現(xiàn)。
type Required<T> = {
[K in keyof T]-?: T[K]
}
type Partial<T> = {
[K in keyof T]+?: T[K]
}
type ReadonlyAndRequired<T> = {
+readonly[K in keyof T]-?: T[K]
}
interface PartialObj {
p?: string
}
type RP = Required<PartialObj> // {p: string}
type RRP = ReadonlyAndRequired<PartialObj> // { readonly p: string}
三、類型變換
3.1 union
union to map
type SetToMap<S extends number | symbol | string, F> = {
[K in S]: F
}
type union = '1' | '2'
type Map = SetToMap<union, number>
union to tuple
// ref: https://github.com/type-challenges/type-challenges/issues/737
1 | 2 => [1, 2]
/**
* UnionToIntersection<{ foo: string } | { bar: string }> =
* { foo: string } & { bar: string }.
*/
type UnionToIntersection<U> = (
U extends unknown ? (arg: U) => 0 : never
) extends (arg: infer I) => 0
? I
: never;
/**
* LastInUnion<1 | 2> = 2.
*/
type LastInUnion<U> = UnionToIntersection<U extends unknown ? (x: U) => 0 : never> extends (x: infer L) => 0
? L
: never;
/**
* UnionToTuple<1 | 2> = [1, 2].
*/
type UnionToTuple<U, Last = LastInUnion<U>> = [U] extends [never]
? []
: [...UnionToTuple<Exclude<U, Last>>, Last];
3.2 tuple
tuple to map
type TupleToMap<T extends readonly any[], P> = {
[K in T[number]]: P
}
const a = [1, 2] as const
type Tuple = typeof a
type union = Tuple[number] // 1 | 2
tuple to union
type TupleToUnion<A extends readonly any[], U = never> = A extends readonly [infer F, ...infer R]
? R extends []
? U | F
: TupleToUnion<R, U | F>
: never
[1,2] => 1 | 2
3.3 map
map to union
type MapToUnion<M> = keyof M
map to tuple
與 union to tuple一致
四、類型體操
類型體操:type-challenges
當(dāng)我們讀完并理解上述內(nèi)容后,應(yīng)該可以輕松完成類型體操的簡單題和中等難度的題~