深入理解 TypeScript
请支持本书:购买捐赠
(广告,请勿屏蔽。)

23 使用类型进行计算概述



在本章中,我们将探讨如何在 TypeScript 中在编译时使用类型进行计算。

请注意,本章的重点是学习如何使用类型进行计算。因此,我们将大量使用字面量类型,并且示例的实际相关性较低。

23.1 类型作为元值

考虑以下两个级别的 TypeScript 代码

类型级别是程序级别的元级别。

级别 可用时间 操作数 操作
程序级别 运行时 函数
类型级别 编译时 特定类型 泛型

使用类型进行计算意味着什么?以下代码是一个示例

type ObjectLiteralType = {
  first: 1,
  second: 2,
};

// %inferred-type: "first" | "second"
type Result = keyof ObjectLiteralType; // (A)

在 A 行中,我们正在采取以下步骤

在类型级别,我们可以使用以下“值”进行计算

type ObjectLiteralType = {
  prop1: string,
  prop2: number,
};

interface InterfaceType {
  prop1: string;
  prop2: number;
}

type TupleType = [boolean, bigint];

//::::: Nullish types and literal types :::::
// Same syntax as values, but they are all types!

type UndefinedType = undefined;
type NullType = null;

type BooleanLiteralType = true;
type NumberLiteralType = 12.34;
type BigIntLiteralType = 1234n;
type StringLiteralType = 'abc';

23.2 泛型:类型的工厂

泛型是元级别的函数 - 例如

type Wrap<T> = [T];

泛型 Wrap<> 具有参数 T。其结果是 T,包装在元组类型中。这就是我们使用此元函数的方式

// %inferred-type: [string]
type Wrapped = Wrap<string>;

我们将参数 string 传递给 Wrap<> 并为结果指定别名 Wrapped。结果是一个只有一个组件的元组类型 - 类型 string

23.3 联合类型和交叉类型

23.3.1 联合类型 (|)

类型运算符 | 用于创建联合类型

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';

// %inferred-type: "a" | "b" | "c" | "d"
type Union = A | B;

如果我们将类型 A 和类型 B 视为集合,则 A | B 是这些集合的集合论并集。换句话说:结果的成员是至少一个操作数的成员。

在语法上,我们也可以在联合类型的第一个组件前面放置一个 |。当类型定义跨越多行时,这很方便

type A =
  | 'a'
  | 'b'
  | 'c'
;
23.3.1.1 联合作为元值集合

TypeScript 将元值集合表示为字面量类型的联合。我们已经看到了一个例子

type Obj = {
  first: 1,
  second: 2,
};

// %inferred-type: "first" | "second"
type Result = keyof Obj;

我们很快就会看到用于循环访问此类集合的类型级别操作。

23.3.1.2 对象类型的联合

由于联合类型的每个成员都是至少一个组件类型的成员,我们只能安全地访问所有组件类型共享的属性(A 行)。要访问任何其他属性,我们需要一个类型守卫(B 行)

type ObjectTypeA = {
  propA: bigint,
  sharedProp: string,
}
type ObjectTypeB = {
  propB: boolean,
  sharedProp: string,
}

type Union = ObjectTypeA | ObjectTypeB;

function func(arg: Union) {
  // string
  arg.sharedProp; // (A) OK
  // @ts-expect-error: Property 'propB' does not exist on type 'Union'.
  arg.propB; // error

  if ('propB' in arg) { // (B) type guard
    // ObjectTypeB
    arg;

    // boolean
    arg.propB;
  }
}

23.3.2 交叉类型 (&)

类型运算符 & 用于创建交叉类型

type A = 'a' | 'b' | 'c';
type B = 'b' | 'c' | 'd';

// %inferred-type: "b" | "c"
type Intersection = A & B;

如果我们将类型 A 和类型 B 视为集合,则 A & B 是这些集合的集合论交集。换句话说:结果的成员是两个操作数的成员。

23.3.2.1 对象类型的交叉

两种对象类型的交叉具有两种类型的属性

type Obj1 = { prop1: boolean };
type Obj2 = { prop2: number };
type Both = {
  prop1: boolean,
  prop2: number,
};

// Type Obj1 & Obj2 is assignable to type Both
// %inferred-type: true
type IntersectionHasBothProperties = IsAssignableTo<Obj1 & Obj2, Both>;

(泛型 IsAssignableTo<> 将在后面解释。)

23.3.2.2 使用交叉类型进行混入

如果我们将对象类型 Named 混入到另一种类型 Obj 中,则我们需要一个交叉类型(A 行)

interface Named {
  name: string;
}
function addName<Obj extends object>(obj: Obj, name: string)
  : Obj & Named // (A)
{
  const namedObj = obj as (Obj & Named);
  namedObj.name = name;
  return namedObj;
}

const obj = {
  last: 'Doe',
};

// %inferred-type: { last: string; } & Named
const namedObj = addName(obj, 'Jane');

23.4 控制流

23.4.1 条件类型

条件类型具有以下语法

«Type2» extends «Type1» ? «ThenType» : «ElseType»

如果 Type2 可分配给 Type1,则此类型表达式的结果为 ThenType。否则,它是 ElseType

23.4.1.1 示例:仅包装具有属性 .length 的类型

在以下示例中,Wrap<> 仅当类型具有属性 .length 且其值为数字时才将它们包装在一个元素的元组中

type Wrap<T> = T extends { length: number } ? [T] : T;

// %inferred-type: [string]
type A = Wrap<string>;

// %inferred-type: RegExp
type B = Wrap<RegExp>;
23.4.1.2 示例:检查可分配性

我们可以使用条件类型来实现可分配性检查

type IsAssignableTo<A, B> = A extends B ? true : false;

// Type `123` is assignable to type `number`
// %inferred-type: true
type Result1 = IsAssignableTo<123, number>;

// Type `number` is not assignable to type `123`
// %inferred-type: false
type Result2 = IsAssignableTo<number, 123>;

有关类型关系可分配性的更多信息,请参阅 [未包含的内容]

23.4.1.3 条件类型是可分配的

条件类型是可分配的:将条件类型 C 应用于联合类型 U 与将 C 应用于 U 的每个组件的联合相同。这是一个例子

type Wrap<T> = T extends { length: number } ? [T] : T;

// %inferred-type: boolean | [string] | [number[]]
type C1 = Wrap<boolean | string | number[]>;

// Equivalent:
type C2 = Wrap<boolean> | Wrap<string> | Wrap<number[]>;

换句话说,可分配性使我们能够“循环”访问联合类型的组件。

这是可分配性的另一个示例

type AlwaysWrap<T> = T extends any ? [T] : [T];

// %inferred-type: ["a"] | ["d"] | [{ a: 1; } & { b: 2; }]
type Result = AlwaysWrap<'a' | ({ a: 1 } & { b: 2 }) | 'd'>;
23.4.1.4 使用可分配条件类型,我们使用类型 never 来忽略事物

解释为集合,类型 never 为空。因此,如果它出现在联合类型中,则会被忽略

// %inferred-type: "a" | "b"
type Result = 'a' | 'b' | never;

这意味着我们可以使用 never 来忽略联合类型的组件

type DropNumbers<T> = T extends number ? never : T;

// %inferred-type: "a" | "b"
type Result1 = DropNumbers<1 | 'a' | 2 | 'b'>;

如果我们交换 then 分支和 else 分支的类型表达式,就会发生这种情况

type KeepNumbers<T> = T extends number ? T : never;

// %inferred-type: 1 | 2
type Result2 = KeepNumbers<1 | 'a' | 2 | 'b'>;
23.4.1.5 内置实用程序类型:Exclude<T, U>

从联合中排除类型是一种非常常见的操作,因此 TypeScript 提供了内置实用程序类型 Exclude<T, U>

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

// %inferred-type: "a" | "b"
type Result1 = Exclude<1 | 'a' | 2 | 'b', number>;

// %inferred-type: "a" | 2
type Result2 = Exclude<1 | 'a' | 2 | 'b', 1 | 'b' | 'c'>;
23.4.1.6 内置实用程序类型:Extract<T, U>

Exclude<T, U> 的反义词是 Extract<T, U>

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

// %inferred-type: 1 | 2
type Result1 = Extract<1 | 'a' | 2 | 'b', number>;

// %inferred-type: 1 | "b"
type Result2 = Extract<1 | 'a' | 2 | 'b', 1 | 'b' | 'c'>;
23.4.1.7 链接条件类型

与 JavaScript 的三元运算符类似,我们也可以链接 TypeScript 的条件类型运算符

type LiteralTypeName<T> =
  T extends undefined ? "undefined" :
  T extends null ? "null" :
  T extends boolean ? "boolean" :
  T extends number ? "number" :
  T extends bigint ? "bigint" :
  T extends string ? "string" :
  never;

// %inferred-type: "bigint"
type Result1 = LiteralTypeName<123n>;

// %inferred-type: "string" | "number" | "boolean"
type Result2 = LiteralTypeName<true | 1 | 'a'>;
23.4.1.8 infer 和条件类型

https://typescript.net.cn/docs/handbook/advanced-types.html#type-inference-in-conditional-types

示例

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

示例

type Syncify<Interf> = {
    [K in keyof Interf]:
        Interf[K] extends (...args: any[]) => Promise<infer Result>
        ? (...args: Parameters<Interf[K]>) => Result
        : Interf[K];
};

// Example:

interface AsyncInterface {
    compute(arg: number): Promise<boolean>;
    createString(): Promise<String>;
}

type SyncInterface = Syncify<AsyncInterface>;
    // type SyncInterface = {
    //     compute: (arg: number) => boolean;
    //     createString: () => String;
    // }

23.4.2 映射类型

映射类型通过循环访问键集合来生成对象 - 例如

// %inferred-type: { a: number; b: number; c: number; }
type Result = {
  [K in 'a' | 'b' | 'c']: number
};

运算符 in 是映射类型的关键部分:它指定了新对象字面量类型的键来自哪里。

23.4.2.1 内置实用程序类型:Pick<T, K>

以下内置实用程序类型允许我们通过指定要保留的现有对象类型的属性来创建新对象

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

它的使用方法如下

type ObjectLiteralType = {
  eeny: 1,
  meeny: 2,
  miny: 3,
  moe: 4,
};

// %inferred-type: { eeny: 1; miny: 3; }
type Result = Pick<ObjectLiteralType, 'eeny' | 'miny'>;
23.4.2.2 内置实用程序类型:Omit<T, K>

以下内置实用程序类型允许我们通过指定要省略的现有对象类型的属性来创建新对象类型

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

说明

Omit<> 的使用方法如下

type ObjectLiteralType = {
  eeny: 1,
  meeny: 2,
  miny: 3,
  moe: 4,
};

// %inferred-type: { meeny: 2; moe: 4; }
type Result = Omit<ObjectLiteralType, 'eeny' | 'miny'>;

23.5 其他各种运算符

23.5.1 索引类型查询运算符 keyof

我们已经遇到过类型运算符 keyof。它列出了对象类型的属性键

type Obj = {
  0: 'a',
  1: 'b',
  prop0: 'c',
  prop1: 'd',
};

// %inferred-type: 0 | 1 | "prop0" | "prop1"
type Result = keyof Obj;

keyof 应用于元组类型会产生可能有点意外的结果

// number | "0" | "1" | "2" | "length" | "pop" | "push" | ···
type Result = keyof ['a', 'b', 'c'];

结果包括

空对象字面量类型的属性键是空集 never

// %inferred-type: never
type Result = keyof {};

这就是 keyof 处理交叉类型和联合类型的方式

type A = { a: number, shared: string };
type B = { b: number, shared: string };

// %inferred-type: "a" | "b" | "shared"
type Result1 = keyof (A & B);

// %inferred-type: "shared"
type Result2 = keyof (A | B);

如果我们记得 A & B 具有类型 A 和类型 B两者的属性,这是有道理的。AB 只有属性 .shared 共享,这解释了 Result2

23.5.2 索引访问运算符 T[K]

索引访问运算符 T[K] 返回 T 的所有属性的类型,其键可分配给类型 KT[K] 也称为查找类型

以下是使用该运算符的示例

type Obj = {
  0: 'a',
  1: 'b',
  prop0: 'c',
  prop1: 'd',
};

// %inferred-type: "a" | "b"
type Result1 = Obj[0 | 1];

// %inferred-type: "c" | "d"
type Result2 = Obj['prop0' | 'prop1'];

// %inferred-type: "a" | "b" | "c" | "d"
type Result3 = Obj[keyof Obj];

括号中的类型必须可分配给所有属性键的类型(由 keyof 计算)。这就是不允许使用 Obj[number]Obj[string] 的原因。但是,如果索引类型具有索引签名,则我们可以使用 numberstring 作为索引类型(A 行)

type Obj = {
  [key: string]: RegExp, // (A)
};

// %inferred-type: string | number
type KeysOfObj = keyof Obj;

// %inferred-type: RegExp
type ValuesOfObj = Obj[string];

KeysOfObj 包括类型 number,因为数字键是 JavaScript(因此也是 TypeScript)中字符串键的子集。

元组类型也支持索引访问

type Tuple = ['a', 'b', 'c', 'd'];

// %inferred-type:  "a" | "b"
type Elements = Tuple[0 | 1];

括号运算符也是可分配的

type MyType = { prop: 1 } | { prop: 2 } | { prop: 3 };

// %inferred-type: 1 | 2 | 3
type Result1 = MyType['prop'];

// Equivalent:
type Result2 =
  | { prop: 1 }['prop']
  | { prop: 2 }['prop']
  | { prop: 3 }['prop']
;

23.5.3 类型查询运算符 typeof

类型运算符 typeof 将(JavaScript)值转换为其(TypeScript)类型。其操作数必须是标识符或点分隔的标识符序列

const str = 'abc';

// %inferred-type: "abc"
type Result = typeof str;

第一个 'abc' 是一个值,而第二个 "abc" 是其类型,一个字符串字面量类型。

这是使用 typeof 的另一个示例

const func = (x: number) => x + x;
// %inferred-type: (x: number) => number
type Result = typeof func;

§14.1.2 “向类型添加符号” 描述了 typeof 的一个有趣用例。