浅析 TypeScript 类型推导

2023-04-19  本文已影响0人  前端艾希

前言

在刚接触 TypeScript 时,我仅仅是对变量,函数进行类型标注,主要也就用到了 typeinterface,泛型等内容;后来因为要开发组件库,于是打开官方文档稍微“进修”了一下,了解了一些工具类型例如 PickReturnTypeExclude 等,以及 tsconfig 的一些编译配置,总的来说也是浅尝辄止。
直到最近开发地图组件库时,产生了一些奇怪的需求,比如:已知有事件 ['click', 'touch', 'close'] ,如何根据这个数组生成一个类型,其属性为 onClickonTouchonClose ,向同事请教后未果,于是决定深入学习下 TypeScript。经过一番学习,实现了一个版本如下:

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>
测试结果

下面,我将分享对学习内容的总结~

一、操作符

keyof

The keyof operator takes an object type and produces a string or numeric literal union of its keys.
keyof 操作符接受一个对象类型,并产生一个字符串或其键的数字字面值联合类型。

参考: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已经有了一个 typeof 操作符,你可以在表达式上下文中使用,TypeScript添加了一个 typeof 操作符,你可以在类型上下文中使用它来引用变量或属性的类型。

参考: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 这个关键字可以延伸一下:JavaScripttypeof 可以帮助 TypeScript 实现类型收紧
除此之外 instanceof 以及 TypeScript 中的 is 也有相同的作用。类型收紧在函数重载中很有用~

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 操作符相关的解释,我只能从实践的角度总结它的作用:TypeScript 中的 in 在对象映射操作上起着至关重要的作用~下文会介绍其具体作用。

infer

TypeScript 中的 infer 用于在泛型类型中推断出其某个参数的类型。通常情况下,我们可以将泛型类型传递给一个具体类型来获取它的类型,但有时候需要从泛型类型中推断出某个输入类型或输出类型,这时候就可以使用 infer 来实现。
注意: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

二、类型基础

2.1 类型

基本类型

基本类型,也可以理解为原子类型。包括 numberbooleanstringnullundefinedfunctionarraysymbol 字面量(truefalse1"a")等,它们无法再细分。

复合类型

复合类型可以分为三类:

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 官方没有提供 union 的取值方式,这也直接导致了和 union 相关的类型变换变得比较复杂。

tuple

因为 tuplereadonly Array<any> 类型,所以 tuple 也可以像数组一样使用数字进行索引。

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 中对象取值的方式一致

interface Object {
    p: string
    q: number
}
type A = Object['p'] // string
type B = Object[keyof Object] // string | number

2.3 遍历方式

TypeScript 的类型系统中无法使用循环语句,所以我们只能用递归来实现遍历,能参与逻辑判断的操作符只有 extends三元运算符 ? ... : ...

union

union 的遍历最简单,只需要用 extends 即可完成。

type Exclude<T, U> = T extends U ? never : T
type A = Exclude<'1' | '2', '2'> // '1'

tuple

元组遍历主要通过 infer 和扩展运算符 ... 实现,通过检查 rest 参数是否为空数组来判断是否递归到最后一项。

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 的最后一个元素
        ? `${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'

字面量数组

字符串的遍历方式和数组类似,也通过 infer 实现,另外还需要模板字符串辅助。

export type Split<
    S extends string,
    P extends string,
    A extends string[] = []
> = S extends `${infer F}${infer R}` ? 
    R extends '' // F 已经是最后一个字符
        ? F extends P
            ? A
            : [...A, F] // F 是一个非分隔符的字符
    : F extends P // F 不是最后一个字符
      ? 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

严格来讲,遍历对象不能称之为“遍历”,而是“映射”,因为一个 map 只能映射成另外一个 map,而不能变成其他的类型~遍历对象主要通过 inkeyof 操作符实现。

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
当我们读完并理解上述内容后,应该可以轻松完成类型体操的简单题和中等难度的题~

上一篇下一篇

猜你喜欢

热点阅读