第 15 章 类型操作

Flying
2023-02-28 / 0 评论 / 9 阅读 / 正在检测是否收录...

条件、映射
拥有超越类型的强大力量
随之而来的是巨大的困惑

TypeScript 为我们提供了在类型系统中定义类型的强大功能。即使是第 10 章“泛型”中的逻辑修饰符,与本章类型操作的功能相比也相形见绌。完成本章后,您将能够基于其他类型混合、匹配和修改类型,从而为您提供在类型系统中表示类型的强大方法。

这些花哨的类型中的大多数都是您通常不想经常使用的技术。您需要了解它们有用的情况,但请注意:当过度使用时,它们可能难以通读。玩得愉快!

映射类型

TypeScript 提供了基于另一种类型的属性创建新类型的语法:换句话说,从一个类型映射到另一个类型。TypeScript 中的映射类型是采用另一种类型 _ 并对该类型的每个属性执行某些操作的类型。

映射类型通过在一组键中的每个键下创建新属性来创建新类型。它们使用类似于索引签名的语法,但不是使用具有 : 的静态键类型(如 [i: string]),而是使用具有 in 的另一种类型的计算类型,如 [OriginalType]

type NewType = {
  [K in OriginalType]: NewProperty;
};

映射类型的一个常见用例是创建一个对象,其键是现有联合类型中的每个字符串文本。此 AnimalCounts 类型创建一个新的对象类型,其中键是 Animals 联合类型的每个值,每个值为 number

type Animals = "alligator" | "baboon" | "cat";


type AnimalCounts = {
  [K in Animals]: number;
};
// Equivalent to:
// {
//    alligator: number;
//    baboon: number;
//    cat: number;
// }

基于现有联合文字的映射类型是在声明大型接口时节省空间的便捷方法。但是,当映射类型可以对其他类型起作用,甚至可以在成员中添加或删除修饰符时,它们确实会大放异彩。

类型映射类型

映射类型通常使用 keyof 运算符对现有类型执行操作,以获取该现有类型的键。通过指示类型映射现有类型的键,我们可以从该现有类型映射到新类型。

AnimalCounts 类型最终与之前的 AnimalCounts 类型相同,方法是从 AnimalVariants 类型映射到新的等效类型:

interface AnimalVariants {
  alligator: boolean;
  baboon: number;
  cat: string;
}

type AnimalCounts = {
  [K in keyof AnimalVariants]: number;
};
// Equivalent to:
// {
//    alligator: number;
//    baboon: number;
//    cat: number;
// }

映射到 keyof(在前面的代码段中名为 K)的新类型键已知是原始类型的键。这意味着允许每个映射的类型成员值在同一键下引用原始类型的相应成员值。

如果原始对象为 SomeName,映射为 [K,键为 SomeName],则映射类型的每个成员都可以将等效的 SomeName 成员的值引用为SomeName [K]

这个 nullablebirdvariant 类型接受一个原始的 birdvariant 类型,并为每个成员添加| null:

interface BirdVariants {
  dove: string;
  eagle: boolean;
}

type NullableBirdVariants = {
  [K in keyof BirdVariants]: BirdVariants[K] | null,
};
// Equivalent to:
// {
//    dove: string | null;
//    eagle: boolean | null;
// }

映射类型允许您定义一组成员,然后根据需要多次大量重新创建它们的新版本,而不是费力地将每个字段从原始类型复制到任意数量的其他类型的其他类型。

映射的类型和签名

在第 7 章 “接口”中,我介绍了 TypeScript 提供了两种将接口成员声明为函数的方法:

  • 方法语法,如 member():void:声明接口的成员是旨在作为对象成员调用的函数
  • 属性语法,如 member: () => void:声明接口的成员等于独立函数

映射类型不区分对象类型的方法语法和属性语法。映射类型将方法视为原始类型的属性。

ResearcherProperties 类型包含 Researcherpropertymethod 成员:

interface Researcher {
  researchMethod(): void;
  researchProperty: () => string;
}

type JustProperties<T> = {
  [K in keyof T]: T[K];
};

type ResearcherProperties = JustProperties<Researcher>;
// Equivalent to:
// {
//    researchMethod: () => void;
//    researchProperty: () => string;
// }

方法和属性之间的区别在大多数实际的 TypeScript 代码中并不经常出现。在实际使用映射类型时,很少会使用类类型。

更改修饰符

映射类型还可以更改原始类型成员上的访问控制修饰符(readonly? 可选性)。readonly? 可以使用与典型接口相同的语法放在映射类型的成员上。

以下 ReadonlyEnvironmentalist 类型使 Environmen talist 接口的一个版本具有给定 readonly 的所有成员,而 OptionalReadonlyConser vationist 更进一步,并创建另一个版本,将 ? 添加到所有 ReadonlyEnvironmentalist 成员:

interface Environmentalist {
  area: string;
  name: string;
}

type ReadonlyEnvironmentalist = {
  readonly [K in keyof Environmentalist]: Environmentalist[K];
};
// Equivalent to:
// {
//    readonly area: string;
//    readonly name: string;
// }

type OptionalReadonlyEnvironmentalist = {
  [K in keyof ReadonlyEnvironmentalist]?: ReadonlyEnvironmentalist[K];
};
// Equivalent to:
// {
//    readonly area?: string;
//    readonly name?: string;
// }
OptionalReadonlyEnvironmentalistreadonly 类型可以写成 readonly[K in keyof Environmentalist]?: Environmentalist[K]

删除修饰符是通过在新类型的修饰符之前添加 - 来完成的。您可以分别写成 -readonly-?:,而不是写成 readonly?:

Conservationist 类型包含 ? 可选和或 readonly 成员,这些成员在 WritableConservationist 中可写,在 RequiredWritableConservationist 中也是必需的:

interface Conservationist {
  name: string; catchphrase?: string; readonly born: number; readonly died?: number;
}

type WritableConservationist = {
  -readonly [K in keyof Conservationist]: Conservationist[K];
};
// Equivalent to:
// {
//    name: string;
//    catchphrase?: string;
//    born: number;
//    died?: number;
// }

type RequiredWritableConservationist = {
  [K in keyof WritableConservationist]-?: WritableConservationist[K];
};
// Equivalent to:
// {
//    name: string;
//    catchphrase: string;
//    born: number;
//    died: number;
// }
RequiredWritableConservationist 类型可以选择性的写成 readonly [K in keyof Environmentalist] ?: Environmentalist[K]

泛型映射类型

映射类型的全部功能来自将它们与泛型相结合,允许在不同类型之间重用一种映射。映射类型能够访问其作用域中的 keyof 任何类型名称,包括映射类型本身上的类型参数。

泛型映射类型通常可用于表示数据在流经应用程序时如何变形。例如,应用程序的某些区域可能希望能够接收现有类型的值,但不允许修改数据。

MakeReadonly 泛型类型接受任何类型,创建一个新版本,并将 readonly 修饰符添加到其所有成员:

type MakeReadonly<T> = {
  readonly [K in keyof T]: T[K];
}

interface Species {
  genus: string; name: string;
}

type ReadonlySpecies = MakeReadonly<Species>;
// Equivalent to:
// {
//    readonly genus: string;
//    readonly name: string;
// }

开发人员通常需要表示的另一个转换是一个函数,该函数接受任意数量的接口,并返回该接口填写完整的实例。

以下 MakeOptional 类型和 createGenusData 函数允许提供任意数量的 GenusData 接口并返回填写了默认值的对象:

interface GenusData {
  family: string; name: string;
}

type MakeOptional<T> = { [K in keyof T]?: T[K];
}
// Equivalent to:
// {
//    family?: string;
//    name?: string;
// }

/**
* Spreads any {overrides} on top of default values for GenusData.
*/
function createGenusData(overrides?: MakeOptional<GenusData>): GenusData {
  return {
    family: 'unknown', name: 'unknown',
    ...overrides,
  }
}

由泛型映射类型完成的某些操作非常有用,以至于 TypeScript 为它们提供了开箱即用的实用程序类型。例如,使用内置 %,可以使所有属性都可选**Partial` 类型。您可以在以下位置找到这些内置类型的列表https://www.typescriptlang.org/docs/handbook/utility-types.html.

条件类型

将现有类型映射到其他类型是很巧妙的,但我们尚未将逻辑条件添加到类型系统。让我们现在开始这样做。

TypeScript 的类型系统是逻辑编程语言的一个例子。它允许基于逻辑检查以前的类型创建新的构造(类型)。它使用条件类型来实现此目的:基于现有类型解析为两种可能类型之一的类型。

条件类型语法看起来像三元运算符:

LeftType extends RightType ? IfTrue : IfFalse

条件类型中的逻辑检查始终取决于左侧类型是扩展还是可分配给右侧类型。

以下 CheckStringAgainstNumber 条件类型检查 string 是否扩展了 number.换句话说,检查 string 类型是否可以分配给 number 类型。它不是,所以结果类型是false(“if false”情况):

// Type: false
type CheckStringAgainstNumber = string extends number ? true : false;

本章其余大部分内容将涉及将其他类型系统功能与条件类型相结合。随着代码片段变得越来越复杂,请记住:每个条件类型纯粹是一段布尔逻辑。每个都采用某种类型,并产生两种可能的结果之一。

泛型条件类型

条件类型能够检查其作用域中的任何类型名称,包括条件类型本身的类型参数。这意味着您可以编写可重用的泛型类型以基于任何其他类型创建新类型。

将前面的 CheckStringAgainstNumber 类型转换为泛型 CheckAgainst Number 会根据前一个类型是否可以分配给 number,给出一个 truefalse 的类型。string 仍然不是 true,而 number0 | 1 都是:

type CheckAgainstNumber<T> = T extends number ? true : false;

// Type: false
type CheckString = CheckAgainstNumber<'parakeet'>;

// Type: true
type CheckString = CheckAgainstNumber<1891>; 

// Type: true
type CheckString = CheckAgainstNumber<number>;

下面的 CallableSetting 类型更有用一些。它接受一个泛型 T 并检查 T 是否是一个函数。如果 T 是,则结果类型为 TGetNumbersSetting 一样,其中 T() => number []。否则,结果类型是是一个返回 T 的函数,与 StringSetting一样,其中 Tstring,因此结果类型为 () => string

type CallableSetting<T> = T extends () => any
  ? T
  : () => T

// Type: () => number[]
type GetNumbersSetting = CallableSetting<() => number[]>;

// Type: () => string
type StringSetting = CallableSetting<string>;

条件类型还能够使用对象成员查找语法访问所提供类型的成员。他们可以在其 extends 子句和或结果类型中使用该信息。

JavaScript 库使用的一种非常适合条件泛型类型的模式是根据提供给函数的选项对象更改函数的返回类型。

例如,许多数据库函数或其等效函数可能会使用类似 throwIfNotFound 的属性来更改函数,以便在未找到值时抛出错误而不是返回 undefined。下面的QueryResult 类型通过生成更窄的 string 而不是 string | undefined 来模拟这种行为,如果选项的 throwIfNotFound 被指定为 true:

interface QueryOptions {
  throwIfNotFound: boolean;
}

type QueryResult<Options extends QueryOptions> = Options["throwIfNotFound"] extends true ? string : string | undefined;

declare function retrieve<Options extends QueryOptions>(key: string,
  options?: Options,
): Promise<QueryResult<Options>>;

// Returned type: string | undefined
await retrieve("Biruté Galdikas");

// Returned type: string | undefined
await retrieve("Jane Goodall", { throwIfNotFound: Math.random() > 0.5 });

// Returned type: string
await retrieve("Dian Fossey", { throwIfNotFound: true });

通过将条件类型与泛型类型参数组合,该 retrieve 函数可以更精确地告诉类型系统它将如何更改其程序的控制流。

类型分布

条件类型分布在联合上,这意味着它们的结果类型将是将该条件类型应用于每个组成部分(联合类型中的类型)的联合。换句话说,ConditionalType<T | U>Conditional<T> | Conditional<U>相同。

类型分布性很难解释,但对于条件类型如何与联合的行为很重要。

请考虑以下 ArrayifyUnlessString 类型,该类型将其类型参数 T 转换为数组,除非 T extends stringHalfArrayified 等效于 string | number[],因为 ArrayifyUnlessString<string | number>ArrayifyUnlessString<string> | ArrayifyUnlessString<number> 相同。

type ArrayifyUnlessString<T> = T extends string ? T : T[];

// Type: string | number[]
type HalfArrayified = ArrayifyUnlessString<string | number>;

如果 TypeScript 的条件类型没有跨联合分布,HalfArrayified 将是 (string | number)[],因为 string |number 不能分配给 string。换句话说,条件类型将其逻辑应用于联合类型的每个组成部分,而不是整个联合类型。

推断类型

访问所提供类型的成员适用于存储为类型成员的信息,但无法捕获其他信息,如函数参数或返回类型。条件类型可以通过在 extends 子句中使用infer关键字来访问条件的任意部分。在 extends 子句中放置 infer 关键字和类型的新名称意味着新类型将在条件类型的 true 情况下可用。

ArrayItems 类型接受类型参数 T,并检查 T 是否是某个新的 Item 类型的数组。如果是,则结果类型为 Item;如果不是,则为 T

type ArrayItems<T> =
  T extends (infer Item)[]
  ? Item
  : T;

// Type: string
type StringItem = ArrayItems<string>;

// Type: string
type StringArrayItem = ArrayItems<string[]>;

// Type: string[]
type String2DItem = ArrayItems<string[][]>;

推断类型也可以用于创建递归条件类型。前面看到的 ArrayItems 类型可以扩展为递归检索任何维度数组的项类型:

type ArrayItemsRecursive<T> = T extends (infer Item)[]
  ? ArrayItemsRecursive<Item>
  : T;

// Type: string
type StringItem = ArrayItemsRecursive<string>;

// Type: string
type StringArrayItem = ArrayItemsRecursive<string[]>;

// Type: string
type String2DItem = ArrayItemsRecursive<string[][]>;

请注意,ArrayItems<string[][]> 的结果是 string[],而 ArrayItemsRecursive<string[][]> 的结果是 string。泛型具有递归的能力,因此可以不断对其进行修改,例如获取数组元素的类型。

映射的条件类型

映射类型将更改应用于现有类型的每个成员。条件类型将更改应用于单个现有类型。它们组合在一起,允许将条件逻辑应用于泛型模板类型的每个成员。

MakeAllMembersFunctions 类型将类型的每个非函数成员转换为函数:

type MakeAllMembersFunctions<T> = {
  [K in keyof T]: T[K] extends (...args: any[]) => any
   ? T[K]
   : () => T[K]
};

type MemberFunctions = MakeAllMembersFunctions<{ alreadyFunction: () => string, notYetFunction: number,
                                               }>;
// Type:
// {
//    alreadyFunction: () => string,
//    notYetFunction: () => number,
// }

映射条件类型是使用某些逻辑检查修改现有类型的所有属性的便捷方法。

never

在第 4 章 “对象” 中,我介绍了 never 类型,即底部类型,这意味着它不可能有值并且无法达到。在正确的位置添加 never 类型注解可以告诉 TypeScript 更积极地检测类型系统以及之前的运行时代码示例中的从未命中的代码路径。

never、交集和联合

描述 never 底部类型的另一种方法是,它是一种不存在的类型。这给了 never 一些有趣的行为,具有 & 交集和 | 联合类型:

  • & 交集类型中的 never 将交集类型减少到仅有 never
  • | 联合类型中的 never 将被忽略。

以下 NeverIntersectionNeverUnion 类型说明了这些行为:

type NeverIntersection = never & string; // Type: never
type NeverUnion = never | string; // Type: string

特别指出的是,在联合类型中被忽略的行为使得 never 对于从条件类型和映射类型中过滤值时非常有用。

never 和条件类型

泛型条件类型通常使用 never 从联合中筛选出类型。由于 never 在联合中被忽略,因此对类型联合的泛型条件的结果将仅是那些不是 never 的类型。

OnlyStrings 泛型条件类型筛选出不是字符串的类型,因此 RedOrBlue 类型从联合中筛选出 0false

type OnlyStrings<T> = T extends string ? T : never;

type RedOrBlue = OnlyStrings<"red" | "blue" | 0 | false>;
// Equivalent to: "red" | "blue"

never 在为泛型类型创建类型实用程序时,通常也与推断的条件类型结合使用。使用 infer 进行类型推断必须是在条件类型的 true 情况下,所以如果 false 情况永远不会被使用,那么 never 是一个合适的类型。

FirstParameter 类型接受函数类型 T,检查它是否是具有 arg: infer Arg 的函数,如果是,则返回该 Arg

type FirstParameter<T extends (...args: any[]) => any> = T extends (arg: infer Arg) => any
  ? Arg
  : never;

type GetsString = FirstParameter<(arg0: string) => void
>; // Type: string

在条件类型的错误情况下使用 nev/er 允许 FirstParameter 提取函数第一个参数的类型。

never 和映射类型

联合中的 never 行为使其对于筛选映射类型中的成员也很有用。可以使用以下三种类型的系统功能筛选出对象的键:

  • never 在联合中被忽略。
  • 映射类型可以映射类型的成员。
  • 如果满足条件,则条件类型可用于将类型转换为 never

将这三者组合在一起,我们可以创建一个映射类型,将原始类型的每个成员更改为原始键或 never。然后,使用 [keyof T] 查询该类型的成员,过滤掉 never,生成所有这些映射类型结果的联合。

以下 OnlyStringProperties 类型将每个 T[K] 成员转换为 K 键(如果该成员是字符串)或 never 键(如果不是):

type OnlyStringProperties<T> = {
  [K in keyof T]: T[K] extends string ? K : never;
}[keyof T];

interface AllEventData {
  participants: string[];
  location: string;
  name: string;
  year: number;
}

type OnlyStringEventData = OnlyStringProperties<AllEventData>;
// Equivalent to: "location" | "name"

读取 OnlyStringProperties 类型的另一种方法是过滤掉所有非字符串属性(将它们切换到 never),然后返回所有剩余的键([keyof T])。

模板字面量类型

我们已经介绍了很多条件和或映射类型。让我们切换到逻辑密集度较低的类型,并专注于字符串一段时间。到目前为止,我已经提出了两种键入字符串值的策略:

  • 基本 string 类型:当值可以是世界上的任何字符串时
  • 字面量类型,如 """abc":当值只能是一种类型(或它们的联合)时

但是,有时您可能希表明字符串与某些字符串模式匹配:字符串的一部分是已知的,但另一部分未知。输入模板字面量类型,这是一种 TypeScript 语法,用于表示字符串类型遵循某种模式。它们看起来像模板字面量字符串(因此得名),但插入了基本类型或基元类型的联合。

此模板字面量类型表明字符串必须以 “Hello” 开头,但可以以任何字符串 (string) 结尾。以 “Hello” 开头的名称,例如 “Hello, world!”匹配,但不匹配“Hello, world!”“hi”

type Greeting = `Hello${string}`;

let matches: Greeting = "Hello, world!"; // Ok

let outOfOrder: Greeting = "World! Hello!";
// ~~~~~~~~~~
// Error: Type '"World! Hello!"' is not assignable to type '`Hello ${string}`'.

let missingAltogether: Greeting = "hi";
// ~~~~~~~~~~~~~~~~~
// Error: Type '"hi"' is not assignable to type '`Hello ${string}`'.

可以在类型插值中使用字符串字面量类型(及其联合),而不是笼统的 string 基本类型,以将模板字面量类型限制为更窄的字符串模式。模板字面量类型在描述必须匹配受限字符串集合的字符串时非常有用。

在这里,BrightnessAndColor 仅匹配以 Brightness 开头、以 Color 结尾且中间有 - 连字符的字符串:

type Brightness = "dark" | "light";
type Color = "blue" | "red";

type BrightnessAndColor = `${Brightness}-${Color}`;
// Equivalent to: "dark-red" | "light-red" | "dark-blue" | "light-blue"

let colorOk: BrightnessAndColor = "dark-blue"; // Ok

let colorWrongStart: BrightnessAndColor = "medium-blue";
// ~~~~~~~~~~~~~~~
// Error: Type '"medium-blue"' is not assignable to type
// '"dark-blue" | "dark-red" | "light-blue" | "light-red"'.

let colorWrongEnd: BrightnessAndColor = "light-green";
// ~~~~~~~~~~~~~
// Error: Type '"light-green"' is not assignable to type
// '"dark-blue" | "dark-red" | "light-blue" | "light-red"'.

如果没有模板文字类型,我们将不得不费力地写出 BrightnessColor 的所有四种组合。如果我们向它们中的任何一个添加更多的字符串字面量,那会变得很麻烦!

TypeScript 允许模板字面量类型包含任何基本类型(symbol 除外)或其联合:stringnumberbigintbooleannullundefined

ExtolNumber 类型允许任何以 “more ” 开头的字符串,包括看起来像数字并以 “wow” 结尾的字符串:

type ExtolNumber = `much ${number} wow`;
function extol(extolee: ExtolNumber) { /* ... */ } extol('much 0 wow'); // Ok
extol('much -7 wow'); // Ok
extol('much 9.001 wow'); // Ok

extol('much false wow');
//    ~~~~~~~~~~~~~~~~
// Error: Argument of type '"much false wow"' is not
// assignable to parameter of type '`much ${number} wow`'.

内部字符串操作类型

为了帮助处理字符串类型,TypeScript 提供了一小组内部(意思是:它们内置于 TypeScript 中)通用实用程序类型,这些实用程序类型接受字符串并对字符串应用一些操作。从 TypeScript 4.7.2 开始,有四个:

  • Uppercase:将字符串字面量类型转换为大写。
  • Lowercase:将字符串字面量类型转换为小写。
  • Capitalize:将字符串字面量类型的第一个字符转换为大写。
  • Uncapitalize:将字符串字面量类型的第一个字符转换为小写。

其中每个都可以用作接受字符串的泛型类型。例如,使用 Capitalize 将字符串中的第一个字母大写:

type FormalGreeting = Capitalize<"hello.">; // Type: "Hello."

这些固有字符串操作类型对于操作对象类型的属性键非常有用。

模板字面量键

模板字面量类型是基本类型string 和字符串字面量之间的中间点,这意味着它们仍然是字符串。它们可以在能使用字符串文本的任何其他位置使用。

例如,您可以将它们用作映射类型中的索引签名。此 ExistenceChecks 类型对 DataKey 中的每个字符串都有一个键,映射为 check${Capitalize}

type DataKey = "location" | "name" | "year";

type ExistenceChecks = {
  [K in `check${Capitalize<DataKey>}`]: () => boolean;
};
// Equivalent to:
// {
//    checkLocation: () => boolean;
//    checkName: () => boolean;
//    checkYear: () => boolean;
// }

function checkExistence(checks: ExistenceChecks) {
  checks.checkLocation(); // Type: boolean checks.checkName(); // Type: boolean

  checks.checkWrong();
  //    ~~~~~~~~~~
  // Error: Property 'checkWrong' does not exist on type 'ExistenceChecks'.
}

重新映射类型键

TypeScript 允许您使用模板字面量类型基于原始成员为映射类型的成员创建新键。在映射类型中放置 as 关键字后跟索引签名的模板字面量类型会更改结果类型的键,以匹配模板字面量类型。这样做允许映射类型对每个映射属性使用不同的键,同时仍引用原始值。

此处,DataEntryGetters 是映射类型,其键为 getLocationgetNamegetYear。每个键都映射到具有模板字面量类型的新键。每个映射值都是一个函数,其返回类型为 DataEntry,使用原始 K 键作为类型参数:

interface DataEntry<T> { 
  key: T;
  value: string;
}

type DataKey = "location" | "name" | "year";

type DataEntryGetters = {
  [K in DataKey as `get${Capitalize<K>}`]: () => DataEntry<K>;
};
// Equivalent to:
// {
//    getLocation: () => DataEntry<"location">;
//    getName: () => DataEntry<"name">;

//    getYear: () => DataEntry<"year">;
// }

键重新映射可以与其他类型操作结合使用,以创建基于现有字面量形状的映射类型。一个有趣的组合是在现有对象上使用 keyof typeof 来使映射类型来脱离该对象的类型。

ConfigGetter 类型基于 config 类型,但每个字段都是一个返回原始配置的函数,并且键是从原始键修改的:

const config = {
  location: "unknown", name: "anonymous", year: 0,
};

type LazyValues = {
  [K in keyof typeof config as `${K}Lazy`]: () => Promise<typeof config[K]>;
};
// Equivalent to:
// {
//    location: Promise<string>;
//    name: Promise<string>;
//    year: Promise<number>;
// }

async function withLazyValues(configGetter: LazyValues) {
  await configGetter.locationLazy; // Resultant type: string

  await configGetter.missingLazy();
  //    ~~~~~~~~~~~
  // Error: Property 'missingLazy' does not exist on type 'LazyValues'.
};

请注意,在 JavaScript 中,对象键可能是 stringSymbol 的类型,Symbol 键不能用作模板字面量类型,因为它们不是基本类型。如果您尝试在泛型类型中使用重新映射的模板字面量类型键,TypeScript 将发出警告,称 symbol 不能在模板字面量类型中使用:

type TurnIntoGettersDirect<T> = {
  [K in keyof T as `get${K}`]: () => T[K]
  //    ~
  // Error: Type 'keyof T' is not assignable to type
  // 'string | number | bigint | boolean | null | undefined'.
  //    Type 'string | number | symbol' is not assignable to type
  //    'string | number | bigint | boolean | null | undefined'.
  //    Type 'symbol' is not assignable to type
  //    'string | number | bigint | boolean | null | undefined'.
};

若要绕过该限制,可以使用 string & 交集类型来强制仅使用可以是字符串的类型。因为 string & symbol 的结果是 never,所以整个模板字符串将减少到 never,TypeScript 将忽略它:

const someSymbol = Symbol("");

interface HasStringAndSymbol {
  StringKey: string;[someSymbol]: number;
}

type TurnIntoGetters<T> = {
  [K in keyof T as `get${string & K}`]: () => T[K]
};

type GettersJustString = TurnIntoGetters<HasStringAndSymbol>;
// Equivalent to:
// {
//    getStringKey: () => string;
// }

TypeScript 从联合中过滤掉 never 类型的行为再次证明自己很有用!

类型操作和复杂性

调试的难度是最初编写代码的两倍。因此,如果您尽可能聪明地根据定义编写代码,但没聪明到能调试它。
——布莱恩·克尼汉

本章中描述的类型操作是当今任何编程语言中最强大、最前沿的类型系统功能之一。大多数开发人员还不够熟悉它们,无法在非常复杂的使用中调试错误。行业标准开发工具(如我在第 12 章“使用 IDE 功能”中介绍的 IDE 功能)通常不是用于可视化相互使用的多层类型操作。

如果您确实需要使用类型操作,为了任何必须阅读您的代码的开发人员(包括将来的您),请尽可能将它们保持在最低限度。使用可读的名称,帮助读者理解代码。如果您认为未来的读者可能遇到困难,请留下描述性的评论。

总结

在本章中,您通过对其类型系统中的类型进行操作来释放 TypeScript 的真正功能:

  • 使用映射类型将现有类型转换为新类型
  • 使用条件类型将逻辑引入类型操作中
  • 了解 never 如何与交集、联合、条件类型和映射类型交互
  • 使用模板字面量类型表示字符串类型的模式
  • 组合模板字面量类型和映射类型以修改类型键
现在您已经读完了这一章,您最好练习一下学到的东西 https://learningtypescript.com/type-operations

当您迷失在类型系统中时,您会使用什么?
映射类型!

0

评论 (0)

取消