TypeScript Office - 类型操纵
# 从类型中创建类型
TypeScript的类型系统非常强大,因为它允许用其他类型的术语来表达类型。
这个想法的最简单的形式是泛型,我们实际上有各种各样的类型操作符可以使用。也可以用我们已经有的值来表达类型。
通过结合各种类型操作符,我们可以用一种简洁、可维护的方式来表达复杂的操作和值。在本节中,我们将介绍用现有的类型或值来表达一个新类型的方法。
- 泛型型:带参数的类型
- Keyof 类型操作符:keyof 操作符创建新类型
- Typeof 类型操作符:使用 typeof 操作符来创建新的类型
- 索引访问类型:使用
Type['a']
语法来访问一个类型的子集 - 条件类型:在类型系统中像if语句一样行事的类型
- 映射类型:通过映射现有类型中的每个属性来创建类型
- 模板字面量类型:通过模板字面字符串改变属性的映射类型
# 泛型
软件工程的一个主要部分是建立组件,这些组件不仅有定义明确和一致的 API,而且还可以重复使用。能够处理今天的数据和明天的数据的组件将为你建立大型软件系统提供最灵活的能力。
在像 C# 和 Java 这样的语言中,创建可重用组件的工具箱中的主要工具之一是泛型,也就是说,能够创建 一个在各种类型上工作的组件,而不是单一的类型。这使得用户可以消费这些组件并使用他们自己的类型。
# Hello World
首先,让我们做一下泛型的 hello world
:身份函数。身份函数是一个函数,它将返回传入的任何内容。你可以用类似于 echo 命令的方式来考虑它。
如果没有泛型,我们将不得不给身份函数一个特定的类型。
function identity(arg: number): number {
return arg;
}
2
3
或者,我们可以用任意类型来描述身份函数。
function identity(arg: any): any {
return arg;
}
2
3
使用 any 当然是通用的,因为它将使函数接受 arg 类型的任何和所有的类型,但实际上我们在函数返回时失去了关于该类型的信息。如果我们传入一个数字,我们唯一的信息就是任何类型都可以被返回。
相反,我们需要一种方法来捕获参数的类型,以便我们也可以用它来表示返回的内容。在这里,我们将使用一个类型变量,这是一种特殊的变量,对类型而不是数值起作用。
function identity<Type>(arg: Type): Type {
return arg;
}
2
3
我们现在已经在身份函数中添加了一个类型变量 Type。这个 Type 允许我们捕获用户提供的类型(例如数字),这样我们就可以在以后使用这些信息。这里,我们再次使用 Type 作为返回类型。经过检查,我们现在可以看到参数和返回类型使用的是相同的类型。这使得我们可以将类型信息从函数的一侧输入,然后从另一侧输出。
我们说这个版本的身份函数是通用的,因为它在一系列的类型上工作。与使用任何类型不同的是,它也和第一个使用数字作为参数和返回类型的身份函数一样精确(即,它不会丢失任何信息)。
一旦我们写好了通用身份函数,我们就可以用两种方式之一来调用它。第一种方式是将所有的参数,包括类型参数,都传递给函数:
let output = identity<string>("myString");
这里我们明确地将 Type 设置为 string,作为函数调用的参数之一,用参数周围的 <>
而不是 ()
来表示。
第二种方式可能也是最常见的。这里我们使用类型参数推理——也就是说,我们希望编译器根据我们传入的参数的类型,自动为我们设置 Type 的值。
let output = identity("myString");
注意,我们不必在角括号(<>
)中明确地传递类型;编译器只是查看了 myString
这个值,并将 Type 设置为其类型。虽然类型参数推断是一个有用的工具,可以使代码更短、更易读,但当编译器不能推断出类型时,你可能需要像我们在前面的例子中那样明确地传入类型参数,这在更复杂的例子中可能发生。
# 使用通用类型变量
当你开始使用泛型时,你会注意到,当你创建像 identity 这样的泛型函数时,编译器会强制要求你在函数主体中正确使用任何泛型参数。也就是说,你实际上是把这些参数当作是任何和所有的类型。
让我们来看看我们前面的 identity 函数。
function identity<Type>(arg: Type): Type {
return arg;
}
2
3
如果我们想在每次调用时将参数 arg 的长度记录到控制台,该怎么办?我们可能很想这样写:
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
2
3
4
当我们这样做时,编译器会给我们一个错误,说我们在使用 arg 的 .length
成员,但我们没有说 arg 有这个成员。记住,我们在前面说过,这些类型的变量可以代表任何和所有的类型,所以使用这个函数的人可以传入一个 number ,而这个数字没有一个 .length
成员。
比方说,我们实际上是想让这个函数在 Type 的数组上工作,而不是直接在 Type 上工作。既然我们在 处理数组,那么 .length
成员应该是可用的。我们可以像创建其他类型的数组那样来描述它。
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
2
3
4
你可以把 loggingIdentity 的类型理解为通用函数 loggingIdentity 接收一个类型参数 Type 和一个参数 arg,arg 是一个 Type 数组,并返回一个 Type 数组。如果我们传入一个数字数组,我们会得到一个数字数组,因为 Type 会绑定到数字。这允许我们使用我们的通用类型变量 Type 作为我们正在处理的类型的一部分,而不是整个类型,给我们更大的灵活性。
我们也可以这样来写这个例子:
function loggingIdentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // 数组有一个.length,所以不会再出错了
return arg;
}
2
3
4
你可能已经从其他语言中熟悉了这种类型的风格。在下一节中,我们将介绍如何创建你自己的通用类型,如 Array<Type>
。
# 泛型类型
在前几节中,我们创建了在一系列类型上工作的通用身份函数。在这一节中,我们将探讨函数本身的类型以及如何创建通用接口。
泛型函数的类型与非泛型函数的类型一样,类型参数列在前面,与函数声明类似:
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;
2
3
4
我们也可以为类型中的通用类型参数使用一个不同的名字,只要类型变量的数量和类型变量的使用方式一致。
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Input>(arg: Input) => Input = identity;
2
3
4
我们也可以把泛型写成一个对象字面类型的调用签名。
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: { <Type>(arg: Type): Type } = identity;
2
3
4
这让我们开始编写我们的第一个泛型接口。让我们把前面例子中的对象字面意思移到一个接口中。
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
2
3
4
5
6
7
在一个类似的例子中,我们可能想把通用参数移到整个接口的参数上。这可以让我们看到我们的泛型是什么类型(例如,Dictionary 而不是仅仅 Dictionary )。这使得类型参数对接口的所有其他成员可见。
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
2
3
4
5
6
7
请注意,我们的例子已经改变了,变成了稍微不同的东西。我们现在没有描述一个泛型函数,而是有一个非泛型的函数签名,它是泛型类型的一部分。当我们使用 GenericIdentityFn 时,我们现在还需要指定相应的类型参数(这里是:数字),有效地锁定了底层调用签名将使用什么。了解什么时候把类型参数直接放在调用签名上,什么时候把它放在接口本身,将有助于描述一个类型的哪些方面是通用的。
除了泛型接口之外,我们还可以创建泛型类。注意,不可能创建泛型枚举和命名空间。
# 泛型类
一个泛型类的形状与泛型接口相似。泛型类在类的名字后面有一个角括号(<>
)中的泛型参数列表。
class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};
2
3
4
5
6
7
8
9
这是对 GenericNumber 类相当直白的使用,但你可能已经注意到,没有任何东西限制它只能使用数字类型。我们本可以使用字符串或更复杂的对象。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
}
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
2
3
4
5
6
就像接口一样,把类型参数放在类本身,可以让我们确保类的所有属性都与相同的类型一起工作。
正如我们在关于类的章节中提到的,一个类的类型有两个方面:静态方面和实例方面。通用类只在其实例侧而非静态侧具有通用性,所以在使用类时,静态成员不能使用类的类型参数。
# 泛型约束
如果你还记得前面的例子,你有时可能想写一个通用函数,在一组类型上工作,而你对这组类型会有什么能力有一定的了解。在我们的 loggingIdentity 例子中,我们希望能够访问 arg.length
属性,但是编译器无法证明每个类型都有一个 .length
属性,所以它警告我们不能做这个假设。
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
return arg;
}
2
3
4
我们希望限制这个函数与 any 和所有类型一起工作,而不是与 any 和所有同时具有 .length
属性的类型一起工作。只要这个类型有这个成员,我们就允许它,但它必须至少有这个成员。要做到这一点,我们必须把我们的要求作为一个约束条件列在 Type 可以是什么。
为了做到这一点,我们将创建一个接口来描述我们的约束。在这里,我们将创建一个接口,它有一个单一的 .length
属性,然后我们将使用这个接和 extends 关键字来表示我们的约束条件。
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length); // 现在我们知道它有一个 .length 属性,所以不再有错误了
return arg;
}
2
3
4
5
6
7
因为泛型函数现在被限制了,它将不再对 any 和 所有的类型起作用。
loggingIdentity(3);
相反,我们需要传入其类型具有所有所需属性的值。
loggingIdentity({ length: 10, value: 3 });
# 在泛型约束中使用类型参数
你可以声明一个受另一个类型参数约束的类型参数。例如,在这里我们想从一个给定名称的对象中获取一个属性。我们想确保我们不会意外地获取一个不存在于 obj 上的属性,所以我们要在这两种类型之间放置一个约束条件。
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
2
3
4
5
6
# 在泛型中使用类类型
在 TypeScript 中使用泛型创建工厂时,有必要通过其构造函数来引用类的类型。比如说:
function create<Type>(c: { new (): Type }): Type {
return new c();
}
2
3
一个更高级的例子,使用原型属性来推断和约束类类型的构造函数和实例方之间的关系。
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
keeper: BeeKeeper = new BeeKeeper();
}
class Lion extends Animal {
keeper: ZooKeeper = new ZooKeeper();
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Keyof 类型操作符
keyof 运算符接收一个对象类型,并产生其键的字符串或数字字面联合。下面的类型 P 与 x | y
是同一类型。
type Point = { x: number; y: number };
type P = keyof Point;
const p1:P = 'x'
const p2:P = 'y'
2
3
4
如果该类型有一个字符串或数字索引签名,keyof 将返回这些类型。
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
const a:A = 0
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
const m:M = 'a'
const m2:M = 10
2
3
4
5
6
7
注意,在这个例子中,M 是 string | number
,这是因为 JavaScript 对象的键总是被强制为字符串,所以 obj[0]
总是与 obj["0"]
相同。
keyof 类型在与映射类型结合时变得特别有用,我们将在后面进一步了解。
# Typeof 类型操作符
JavaScript 已经有一个 typeof 操作符,你可以在表达式上下文中使用。
// 输出 "string"
console.log(typeof "Hello world");
2
TypeScript添加了一个 typeof 操作符,你可以在类型上下文中使用它来引用一个变量或属性的类型。
let s = "hello";
let n: typeof s;
n = 'world'
n= 100 // 报错:不能将类型"number"分配给类型"string"。
2
3
4
这对基本类型来说不是很有用,但结合其他类型操作符,你可以使用 typeof 来方便地表达许多模式。举一个例子,让我们先看看预定义的类型 ReturnType。它接收一个函数类型并产生其返回类型:
type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>;
2
如果我们试图在一个函数名上使用 ReturnType,我们会看到一个指示性的错误。
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<f>;
2
3
4
请记住,值和类型并不是一回事。为了指代值 f 的类型,我们使用 typeof。
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;
2
3
4
TypeScript 故意限制了你可以使用 typeof 的表达式种类。
具体来说,只有在标识符(即变量名)或其属性上使用 typeof 是合法的。这有助于避免混乱的陷阱,即编写你认为是在执行的代码,但其实不是。
// 我们认为使用 = ReturnType<typeof msgbox>
let shouldContinue: typeof msgbox("Are you sure you want to continue?");
2
# 索引访问类型
我们可以使用一个索引访问类型来查询另一个类型上的特定属性:
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];
2
索引类型本身就是一个类型,所以我们可以完全使用 unions、keyof 或者其他类型。
interface Person {
name: string
age: number
alive: boolean
}
// type I1 = string | number
type I1 = Person["age" | "name"];
const i11:I1 = 100
const i12:I1 = ''
// type I2 = string | number | boolean
type I2 = Person[keyof Person];
const i21:I2 = ''
const i22:I2 = 100
const i23:I2 = false
// type I3 = Person[AliveOrName];
type AliveOrName = "alive" | "name";
const aon1:AliveOrName = 'alive'
const aon2:AliveOrName = 'name'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
如果你试图索引一个不存在的属性,你甚至会看到一个错误:
type I1 = Person["alve"];
另一个使用任意类型进行索引的例子是使用 number 来获取一个数组元素的类型。我们可以把它和 typeof 结合起来,方便地获取一个数组字面的元素类型。
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
/* type Person = {
name: string;
age: number;
} */
type Person = typeof MyArray[number];
const p:Person = {
name: 'xiaoqian',
age: 11
}
// type Age = number
type Age = typeof MyArray[number]["age"];
const age:Age = 11
// 或者
// type Age2 = number
type Age2 = Person["age"];
const age2:Age2 = 11
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
你只能在索引时使用类型,这意味着你不能使用 const 来做一个变量引用:
const key = "age";
type Age = Person[key];
2
然而,你可以使用类型别名来实现类似的重构风格:
type key = "age";
type Age = Person[key];
2
# 条件类型
在大多数有用的程序的核心,我们必须根据输入来做决定。JavaScript 程序也不例外,但鉴于数值可以很容易地被内省,这些决定也是基于输入的类型。条件类型有助于描述输入和输出的类型之间的关系。
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
// type Example1 = number
type Example1 = Dog extends Animal ? number : string;
// type Example2 = string
type Example2 = RegExp extends Animal ? number : string;
2
3
4
5
6
7
8
9
10
条件类型的形式看起来有点像 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression
)。
SomeType extends OtherType ? TrueType : FalseType;
当 extends 左边的类型可以赋值给右边的类型时,那么你将得到第一个分支中的类型(真分支);否则你将得到后一个分支中的类型(假分支)。
从上面的例子来看,条件类型可能并不立即显得有用:我们可以告诉自己是否 Dog extends Animal
,并选择 number 或 string,但条件类型的威力来自于它所带来的好处。条件类型的力量来自于将它们与泛型一起使用。
例如,让我们来看看下面这个 createLabel 函数:
interface IdLabel {
id: number /* 一些字段 */;
}
interface NameLabel {
name: string /* 另一些字段 */;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}
2
3
4
5
6
7
8
9
10
11
12
createLabel 的这些重载描述了一个单一的 JavaScript 函数,该函数根据其输入的类型做出选择。注意一些事情:
- 如果一个库必须在其 API 中反复做出同样的选择,这就会变得很麻烦
- 我们必须创建三个重载:一个用于确定类型的情况(一个用于 string,一个用于 number),一个用于最一般的情况(取一个
string | number
)。对于 createLabel 所能处理的每一种新类型,重载的数量都会呈指数级增长
相反,我们可以在一个条件类型中对该逻辑进行编码:
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
2
3
然后我们可以使用该条件类型,将我们的重载简化为一个没有重载的单一函数。
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
// let a: NameLabel
let a = createLabel("typescript");
// let b: IdLabel
let b = createLabel(2.8);
// let c: NameLabel | IdLabel
let c = createLabel(Math.random() ? "hello" : 42);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 条件类型约束
通常,条件类型中的检查会给我们提供一些新的信息。就像用类型守卫缩小范围可以给我们一个更具体的类型一样,条件类型的真正分支将通过我们检查的类型进一步约束泛型。
例如,让我们来看看下面的例子:
type MessageOf<T> = T["message"];
在这个例子中,TypeScript 出错是因为 T 不知道有一个叫做 message 的属性。我们可以对 T 进行约束,TypeScript 就不会再抱怨。
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>;
2
3
4
5
然而,如果我们想让 MessageOf 接受任何类型,并在消息属性不可用的情况下,默认为 never 类型呢?我们可以通过将约束条件移出,并引入一个条件类型来做到这一点。
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
// type EmailMessageContents = string
type EmailMessageContents = MessageOf<Email>;
const emc:EmailMessageContents = 'balabala...'
// type DogMessageContents = never
type DogMessageContents = MessageOf<Dog>;
const dmc:DogMessageContents = 'error' as never
2
3
4
5
6
7
8
9
10
11
12
13
在真正的分支中,TypeScript 知道 T 会有一个消息属性。
作为另一个例子,我们也可以写一个叫做 Flatten 的类型,将数组类型平铺到它们的元素类型上,但在其他方面则不做处理。
type Flatten<T> = T extends any[] ? T[number] : T;
// 提取出元素类型。
// type Str = string
type Str = Flatten<string[]>;
// 单独一个类型。
// type Num = number
type Num = Flatten<number>;
2
3
4
5
6
7
当 Flatten 被赋予一个数组类型时,它使用一个带有数字的索引访问来获取 string[]
的元素类型。否则,它只是返回它被赋予的类型。
# 在条件类型内进行推理
我们只是发现自己使用条件类型来应用约束条件,然后提取出类型。这最终成为一种常见的操作,而条件类型使它变得更容易。
条件类型为我们提供了一种方法来推断我们在真实分支中使用 infer 关键字进行对比的类型。例如,我们可以在 Flatten 中推断出元素类型,而不是用索引访问类型「手动」提取出来。
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
在这里,我们使用 infer 关键字来声明性地引入一个名为 Item 的新的通用类型变量,而不是指定如何在真实分支中检索 T 的元素类型。这使我们不必考虑如何挖掘和探测我们感兴趣的类型的结构。
我们可以使用 infer 关键字编写一些有用的辅助类型别名。例如,对于简单的情况,我们可以从函数类型中提取出返回类型。
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
? Return
: never;
// type Num = number
type Num = GetReturnType<() => number>;
// type Str = string
type Str = GetReturnType<(x: string) => string>;
// type Bools = boolean[]
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
// 给泛型传入 string 类型,条件类型会返回 never
type Never = GetReturnType<string>
const nev:Never = 'error' as never
2
3
4
5
6
7
8
9
10
11
12
当从一个具有多个调用签名的类型(如重载函数的类型)进行推断时,从最后一个签名进行推断(据推测,这是最容许的万能情况)。不可能根据参数类型的列表来执行重载解析。
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
// type T1 = string | number
type T1 = ReturnType<typeof stringOrNum>;
2
3
4
5
# 分布式条件类型
当条件类型作用于一个通用类型时,当给定一个联合类型时,它们就变成了分布式的。例如,以下面的例子为例:
type ToArray<Type> = Type extends any ? Type[] : never;
如果我们将一个联合类型插入 ToArray,那么条件类型将被应用于该联合的每个成员。
type ToArray<Type> = Type extends any ? Type[] : never;
// type StrArrOrNumArr = string[] | number[]
type StrArrOrNumArr = ToArray<string | number>;
2
3
这里发生的情况是,StrArrOrNumArr 分布在:
string | number;
并对联合的每个成员类型进行映射,以达到有效的目的:
ToArray<string> | ToArray<number>;
这给我们留下了:
string[] | number[];
通常情况下,分布性是需要的行为。为了避免这种行为,你可以用方括号包围 extends 关键字的每一边。
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'StrArrOrNumArr'不再是一个联合类型
// type StrArrOrNumArr = (string | number)[]
type StrArrOrNumArr = ToArrayNonDist<string | number>;
2
3
4
# 映射类型
当你不想重复定义类型,一个类型可以以另一个类型为基础创建新类型。
映射类型建立在索引签名的语法上,索引签名用于声明没有被提前声明的属性类型。
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
};
2
3
4
5
6
7
映射类型是一种通用类型,它使用 PropertyKeys 的联合(经常通过 keyof 创建)迭代键来创建一个类型。
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
2
3
在这个例子中,OptionsFlags 将从 Type 类型中获取所有属性,并将它们的值改为布尔值。
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
};
/*
type FeatureOptions = {
darkMode: boolean;
newUserProfile: boolean;
}
*/
type FeatureOptions = OptionsFlags<FeatureFlags>;
2
3
4
5
6
7
8
9
10
11
# 映射修改器
在映射过程中,有两个额外的修饰符可以应用:readonly
和 ?
,它们分别影响可变性和可选性。
你可以通过用 - 或 + 作为前缀来删除或添加这些修饰语。如果你不加前缀,那么就假定是 +。
type CreateMutable<Type> = {
// 从一个类型的属性中删除 "readonly"属性
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
/*
type UnlockedAccount = {
id: string;
name: string;
}
*/
type UnlockedAccount = CreateMutable<LockedAccount>;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 从一个类型的属性中删除 "可选" 属性
type Concrete<Type> = {
[Property in keyof Type]-?: Type[Property];
};
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
/*
type User = {
id: string;
name: string;
age: number;
}
*/
type User = Concrete<MaybeUser>;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 通过 as 做 key 重映射
在 TypeScript 4.1 及以后的版本中,你可以通过映射类型中的 as 子句重新映射映射类型中的键。
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
2
3
你可以利用模板字面类型等功能,从先前的属性名称中创建新的属性名称。
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () =>
Type[Property]
};
interface Person {
name: string;
age: number;
location: string;
}
/*
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
*/
type LazyPerson = Getters<Person>;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
你可以通过条件类型产生 never 滤掉的键。
// 删除 "kind"属性
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
/*
type KindlessCircle = {
radius: number;
}
*/
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
2
3
4
5
6
7
8
9
10
11
12
13
14
你可以映射任意的联合体,不仅仅是 string | number | symbol
的联合体,还有任何类型的联合体。
type EventConfig<Events extends { kind: string }> = {
[E in Events as E["kind"]]: (event: E) => void;
}
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
/*
type Config = {
square: (event: SquareEvent) => void;
circle: (event: CircleEvent) => void;
}
*/
type Config = EventConfig<SquareEvent | CircleEvent>
2
3
4
5
6
7
8
9
10
11
12
# 进一步探索
映射类型与本类型操作部分的其他功能配合得很好,例如,这里有一个使用条件类型的映射类型 ,它根据一个对象的属性 pii 是否被设置为字面意义上的 true ,返回 true 或 false 。
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
/*
type ObjectsNeedingGDPRDeletion = {
id: false;
name: true;
}
*/
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
type ObjectsNeedingGDPRDeletion = ExtractPII<DBFields>
2
3
4
5
6
7
8
9
10
11
12
13
14