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

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 = {  // 如果只进行类型编程,参数的大小写就无所谓了
  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 = 
  (() => A extends A2 ? 1 : 0) extends (() => A extends A1 ? 1 : 0) 
  ? 1 
  : 0;

参考 github issue: type level equal operator

3. 类型编程

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

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

3.1 分支判断

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

例如,

type func = 
  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, ...tail];

type join
  = strs extends [] ? result
  : strs extends array ? `${result}${head}`
  : strs extends array ? join
  : 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
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 [] ? result
  : strs extends [infer head] ? `${result}${head}`
  : strs extends [infer head, ...infer tail] ? join
  : never;

4. keyofin

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

例子,

type func = {
  [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 = 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];  // 获取属性值对应值的 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 = {
  [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 = {
    [P in keyof T]?: T[P];
};

// 所有字段变成必填:{a?: number, b:string} -> {a:number, b:string}
// 已经必填的不受影响
type Required = {
    [P in keyof T]-?: T[P];
};

// 所有字段变成 readonly
type Readonly = {
    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 = {
    [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 = {
    [P in K]: T;
};

// 计算差集 T-U
// 判断 T 中(union)的每一个部分,是否是 U 的子类型,是就去掉,否则留下,最后将结果 union 起来
// Exclude<'a'|'b', 'b'|'c'> -> 'a'
type Exclude = T extends U ? never : T;

// 计算 交集
// Extract<'a'|'b', 'b'|'c'> -> 'b'
type Extract = T extends U ? T : never;

// 从 T 中去掉部分 props
// Omit<{a:1,b:2,c:3}, 'a'|'c'> -> {b:2}
type Omit = Pick>;

// 约束 T 不能是 null 或 undefined
type NonNullable = T extends null | undefined ? never : T;

// 通过模式匹配 infer,获得函数的参数类型
type Parameters any> = T extends (...args: infer P) => any ? P : never;

// 获取 constructor 的参数类型
// abstract 指的是 https://www.tutorialsteacher.com/typescript/abstract-class
type ConstructorParameters any> = T extends abstract new (...args: infer P) => any ? P : never;

// 获取函数的返回值类型
type ReturnType any> = T extends (...args: any) => infer R ? R : any;

// 获取构造函数示例的类型
type InstanceType any> = T extends abstract new (...args: any) => infer R ? R : any;

// 将 S 转换成大写(intrinsic 表示需要 TypeScript 内部来实现)
type Uppercase = intrinsic;
// 将 S 转换成小写
type Lowercase = intrinsic;
// 将 S 转换成 首字母大写
type Capitalize = intrinsic;
// 将 S 转换成首字母小写
type Uncapitalize = intrinsic;

6. 加法运算

type L
  = r['length'] extends n ? r
  : L

type add
  = [...L, ...L]['length'];

type minus
  = L extends [...head: L, ...tail: infer z] ? z['length']
  : never;

type mul
  = y extends 0 ? 0
  : mul> extends infer r
    ? r extends number ? add  // 手工限定递归步骤的类型为 number
    : never
  : never;
import { Test } from 'ts-toolbelt';
const { checks, check } = Test;

checks([
  check<
    mul<2, 3>,  // 实现类型上的乘法
    6,
    Test.Pass
  >(),
]);

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

type mul
  = y extends 0 ? 0
  : add>, 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 类型编程(小结))