淺析 TypeScript 類型推導(dǎo)

前言

在剛接觸 TypeScript 時(shí),我僅僅是對(duì)變量,函數(shù)進(jìn)行類型標(biāo)注,主要也就用到了 type、interface,泛型等內(nèi)容;后來因?yàn)橐_發(fā)組件庫,于是打開官方文檔稍微“進(jìn)修”了一下,了解了一些工具類型例如 Pick、ReturnTypeExclude 等,以及 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>
測(cè)試結(jié)果

下面,我將分享對(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)鍵字可以延伸一下:JavaScripttypeof 可以幫助 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、booleanstring、null、undefined、functionarray、symbol 字面量(true,false1,"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)?tuplereadonly 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ì)象主要通過 inkeyof 操作符實(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)該可以輕松完成類型體操的簡單題和中等難度的題~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容