美文网首页
浅析 TypeScript 类型推导

浅析 TypeScript 类型推导

作者: 前端艾希 | 来源:发表于2023-04-19 00:02 被阅读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")等,它们无法再细分。

    复合类型

    复合类型可以分为三类:

    • union,指一个无序的、无重复元素的集合。
    • tuple,可简单看做一个只读数组的类型。
    • map,和 JavaScript 中的对象一样,是一些没有重复键的键值对。
    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
    当我们读完并理解上述内容后,应该可以轻松完成类型体操的简单题和中等难度的题~

    相关文章

      网友评论

          本文标题:浅析 TypeScript 类型推导

          本文链接:https://www.haomeiwen.com/subject/jamkjdtx.html