美文网首页Front End
[FE] TypeScript 类型编程(小结)

[FE] TypeScript 类型编程(小结)

作者: 何幻 | 来源:发表于2021-11-07 13:09 被阅读0次

    1. TypeScript 基本类型

    TypeScript 有以下这些基本类型:string, number, boolean
    单个的值也是看做类型,1, 'a', null, true

    类型可以看做是值的 “集合”。
    Effective TypeScript: (Item 7)Think of Types as Sets of Values

    2. 构造新类型

    2.1 类型运算

    类型之间可以组合成新的类型,

    • 交集:&
    • 并集:|
    • 类型构造器:数组 [], interface {}, 新的字符串类型 ${a}${b}

    2.2 类型函数

    可以借用泛型实现类型上的函数,进行类型变换,把入参类型,转换成出参类型,

    type func<x extends string, y extends string> = {  // 如果只进行类型编程,参数的大小写就无所谓了
      result: [x, y, `${x}-${y}`]
    };
    

    a extends b 用来判断 类型 a 是否 b 的子类型(子集),

    • 用于 类型函数的参数中,表示对类型参数进行限制
    • 用于 类型函数体中,表示分支判断(下文介绍)

    可以用 ts-toolbelt 跑一下测试,

    import { Test } from 'ts-toolbelt';
    const { checks, check } = Test;
    
    checks([
      check<  // 判断两个类型是否相等
        func<'a', 'b'>,
        {
          result: ['a', 'b', `a-b`]
        },
        Test.Pass
      >(),
    ]);
    

    值得一提是,类型相等性判断不同人可能会有不同做法,ts-toolbelt/Equals 中用的是,

    type Equals<A1 extends any, A2 extends any> = 
      (<A>() => A extends A2 ? 1 : 0) extends (<A>() => A extends A1 ? 1 : 0) 
      ? 1 
      : 0;
    

    参考 github issue: type level equal operator

    3. 类型编程

    我们知道编程语言的控制结构包括三种:顺序、选择、循环。

    • 顺序:使用 类型函数(上文介绍了)
    • 选择:使用 extends
    • 循环:使用 类型函数 的递归

    3.1 分支判断

    在类型函数中,可以使用 extends 来实现分支判断(使用 infer 可实现模式匹配)。

    例如,

    type func<str> = 
      str extends `${infer first}${infer tail}`
      ? [first, tail]
      : unknown
    
    import { Test } from 'ts-toolbelt';
    const { checks, check } = Test;
    
    checks([
      check<  // 判断两个类型是否相等
        func<'abc'>,
        ['a', 'bc'],
        Test.Pass
      >(),
    ]);
    

    3.1 循环(递归)

    type array<head extends string, tail extends string[]>
      = [head, ...tail];
    
    type join<strs extends string[], result extends string>
      = strs extends [] ? result
      : strs extends array<infer head, []> ? `${result}${head}`
      : strs extends array<infer head, infer tail> ? join<tail, `${result}${head}`>
      : never;
    
    import { Test } from 'ts-toolbelt';
    const { checks, check } = Test;
    
    checks([
      check<
        join<['hello', ' ', 'world'], ''>,
        'hello world',
        Test.Pass
      >(),
    ]);
    

    其中引入了辅助函数 array,是为了限定 headtail 的类型。
    否则会报以下错误,

    (1)`${result}${head}`
    Type 'head' is not assignable to type 'string | number | bigint | boolean'.
      Type 'head' is not assignable to type 'number'.ts(2322)
    
    (2)join<tail, `${result}${head}`>
    Type 'tail' does not satisfy the constraint 'string[]'.
      Type 'unknown[]' is not assignable to type 'string[]'.
        Type 'unknown' is not assignable to type 'string'.ts(2344)
    Type 'head' is not assignable to type 'string | number | bigint | boolean'.
      Type 'head' is not assignable to type 'number'.ts(2322)
    

    出错的示例如下,

    type join<strs extends string[], result extends string>
      = strs extends [] ? result
      : strs extends [infer head] ? `${result}${head}`
      : strs extends [infer head, ...infer tail] ? join<tail, `${result}${head}`>
      : never;
    

    4. keyofin

    在学习 Mapped Types 的时候,
    经常会看到 keyofin 两个操作符,曾经造成过一些困扰,这里总结如下。

    例子,

    type func<input> = {
      [props in keyof input]: {  // 这里 keyof input 为:'a' | 'b'
        [field in props]: input[field]  // props 分别为 'a'(可以看做只有一个类型的 union) 和 'b'
      }
    }
    
    import { Test } from 'ts-toolbelt';
    const { checks, check } = Test;
    
    checks([
      check<
        func<{ a: number, b: string }>,
        { a: { a: number }, b: { b: string } },
        Test.Pass
      >(),
    ]);
    

    这里有几个值得注意的点:

    (1)通过 keyof 获取属性类型的 union

    type func<obj> = keyof obj;  // 所有属性名构成的 union
    
    import { Test } from 'ts-toolbelt';
    const { checks, check } = Test;
    
    checks([
      check<
        func<{ a: number, b: string }>,
        'a' | 'b',
        Test.Pass
      >(),
    ]);
    

    (2)值类型的 union

    type func<obj, k extends keyof obj> = obj[k];  // 获取属性值对应值的 union【k 是一个 union】
    
    import { Test } from 'ts-toolbelt';
    const { checks, check } = Test;
    
    checks([
      check<
        func<{ a: number, b: string, c: boolean }, 'a' | 'c'>,
        number | boolean,
        Test.Pass
      >(),
    ]);
    

    (3)使用 in 进行循环操作

    type func<obj, k extends keyof obj> = {
      [prop in k]: obj[prop]
    };
    
    import { Test } from 'ts-toolbelt';
    const { checks, check } = Test;
    
    checks([
      check<
        func<{ a: number, b: string, c: boolean }, 'a' | 'c'>,
        { a: number, c: boolean },
        Test.Pass
      >(),
    ]);
    

    5. 内置函数

    TypeScript 内置了一些 类型函数,称为 Utility Types
    位于 typescript/lib/lib.es5.d.ts L1471-L1561

    本地位置通常在这里,

    /Applications/Visual Studio Code.app/Contents/Resources/app/extensions/node_modules/typescript/lib/lib.es5.d.ts
    

    我们来学习一下这些内置函数的实现,

    // 所有字段变成可选:{a: number, b?:string} -> {a?:number, b?:string}
    // 已经可选的不受影响
    type Partial<T> = {
        [P in keyof T]?: T[P];
    };
    
    // 所有字段变成必填:{a?: number, b:string} -> {a:number, b:string}
    // 已经必填的不受影响
    type Required<T> = {
        [P in keyof T]-?: T[P];
    };
    
    // 所有字段变成 readonly
    type Readonly<T> = {
        readonly [P in keyof T]: T[P];
    };
    
    // 过滤 interface T,只留下给定 prop
    // Pick<{a:1,b:2,c:3}, 'a'|'c'> -> {a:1,c:3}
    type Pick<T, K extends keyof T> = {
        [P in K]: T[P];
    };
    
    // 这里必须限定 K 是 `extends keyof any`,否则会报错
    // Type 'K' is not assignable to type 'string | number | symbol'.
    //  Type 'K' is not assignable to type 'symbol'.ts(2322)
    // Record<'a'|'b', 1> -> {a:1,b:1}
    type Record<K extends keyof any, T> = {
        [P in K]: T;
    };
    
    // 计算差集 T-U
    // 判断 T 中(union)的每一个部分,是否是 U 的子类型,是就去掉,否则留下,最后将结果 union 起来
    // Exclude<'a'|'b', 'b'|'c'> -> 'a'
    type Exclude<T, U> = T extends U ? never : T;
    
    // 计算 交集
    // Extract<'a'|'b', 'b'|'c'> -> 'b'
    type Extract<T, U> = T extends U ? T : never;
    
    // 从 T 中去掉部分 props
    // Omit<{a:1,b:2,c:3}, 'a'|'c'> -> {b:2}
    type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
    
    // 约束 T 不能是 null 或 undefined
    type NonNullable<T> = T extends null | undefined ? never : T;
    
    // 通过模式匹配 infer,获得函数的参数类型
    type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
    
    // 获取 constructor 的参数类型
    // abstract 指的是 https://www.tutorialsteacher.com/typescript/abstract-class
    type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;
    
    // 获取函数的返回值类型
    type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
    
    // 获取构造函数示例的类型
    type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
    
    // 将 S 转换成大写(intrinsic 表示需要 TypeScript 内部来实现)
    type Uppercase<S extends string> = intrinsic;
    // 将 S 转换成小写
    type Lowercase<S extends string> = intrinsic;
    // 将 S 转换成 首字母大写
    type Capitalize<S extends string> = intrinsic;
    // 将 S 转换成首字母小写
    type Uncapitalize<S extends string> = intrinsic;
    

    6. 加法运算

    type L<n extends number, r extends never[]>
      = r['length'] extends n ? r
      : L<n, [never, ...r]>
    
    type add<x extends number, y extends number>
      = [...L<x, []>, ...L<y, []>]['length'];
    
    type minus<x extends number, y extends number>
      = L<x, []> extends [...head: L<y, []>, ...tail: infer z] ? z['length']
      : never;
    
    type mul<x extends number, y extends number>
      = y extends 0 ? 0
      : mul<x, minus<y, 1>> extends infer r
        ? r extends number ? add<r, x>  // 手工限定递归步骤的类型为 number
        : never
      : never;
    
    import { Test } from 'ts-toolbelt';
    const { checks, check } = Test;
    
    checks([
      check<
        mul<2, 3>,  // 实现类型上的乘法
        6,
        Test.Pass
      >(),
    ]);
    

    值得一提的是 mul 的实现,以下实现方式会报错,

    type mul<x extends number, y extends number>
      = y extends 0 ? 0
      : add<mul<x, minus<y, 1>>, x>;  // Type instantiation is excessively deep and possibly infinite. ts(2589)
    

    解决方案 参考 Type instantiation is excessively deep and possibly infinite. ts(2589)


    参考

    TypeScript Type-Level Programming
    用 TypeScript 模板字面类型来制作 URL parser
    用 TypeScript 类型运算实现一个中国象棋程序
    TypeScript 类型体操天花板,用类型运算写一个 Lisp 解释器

    相关文章

      网友评论

        本文标题:[FE] TypeScript 类型编程(小结)

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