概述
软件工程的主要部分就是构建拥有明确且连续的 APIs 以及可复用的组件。组件能够处理现在和将来的数据,这为你构建大型软件系统提供了强大的扩展性。
像 C# 和 Java 这样的语言,用于创建可复用组件的工具箱中主要的工具就是泛型,它能够创建一个可以在多种类型上工作的组件。这使得用户能够使用自己的类型来消费这些组件。
泛型之 Hello World
让我们从"hello world" 开始泛型的学习:identity 函数。identity 是一个原样返回入参的函数。你可以用类似于 echo 命令的方式来思考这个问题。
没有泛型,我们不得不给 identity 函数一个特定的类型:
function identity(arg: number): number {
return arg;
}
或者,我们可以使用 any
类型描述 identity 函数:
function identity(arg: any): any {
return arg;
}
使用 any
当然是泛型,因为这会让函数参数 arg
的类型接受 any 和所有类型,事实上我们正在丢失函数返回值的类型。如果我们传入一个数字,关于返回值的唯一信息就是它可以是任意类型。
然而,我们需要一种获取参数类型的方式来标识返回值的类型。这里,我们使用类型变量,一种针对于类型而不是值的特殊变量。
function identity<Type>(arg: Type): Type {
return arg;
}
我们为 identity
函数添加了一个类型变量 Type
。Type
允许我们捕获用户提供的值的类型(比如. number
),这样我们就可以在后面使用这个类型信息。这里,我们再次使用 Type
作为返回类型。这允许我们在函数的一端传输类型信息并在其他地方接收。
我们说这个版本的 identity
函数就是泛型,因为它适用于一系列类型。不像使用 any
,它和第一个使用数字作为参数和返回类型的 identity
函数一样精确(比如,它不会丢失任何信息)。
一旦我们定义了泛型 identity 函数,我们就可以用两种方式调用它。第一种方法是将所有参数(包括 type 参数)传递给函数:
let output = identity<string>("mystring");
// ^ = let output: string
这里我们显式地将 Type
设置为 string
作为函数调用的其中一个参数,用 <>
而不是 ()
包裹参数。
第二种方式可能也是最常见的。这里我们使用类型参数推断(type argument inference)-- 也就是说,我们希望编译器根据传入的参数类型自动为我们设置 type
的值:
let output = identity("myString");
// ^ = let output: string
注意,我们不必显式地传递尖括号(<>
)中的类型;编译器只查看值 "mystring"
,并将 Type
设置为它的类型。而类型参数推断是一种非常有用的工具,可以使代码更简洁,更易于阅读,当编译器无法推断出类型时,你可能需要显式地传入类型参数,就像我们在前面的例子中所做的那样,这在更复杂的例子中可能会发生。
使用泛型类型变量
当你开始使用泛型时,你会注意到,当你创建像 identity
这样的泛型函数时,编译器会强制你正确地在函数体中使用任何泛型类型的参数。也就是说,实际上可以将这些参数视为任意类型或所有类型。
让我们回到早前的 identity
函数:
function identity<Type>(arg: Type): Type {
return arg;
}
如果我们还想在每次调用时向控制台记录参数 arg
的长度,该怎么办?我们可能会忍不住这样写:
function identity<Type>(arg: Type): Type {
console.log(arg.length);
// Property 'length' does not exist on type 'Type'.
return arg;
}
当我们这样做的时候,编译器会抛出一个错误,提示我们使用了 arg
的 .length
成员,但是我们没有指明 arg
有这个成员。记住,我们前面说过,这些类型变量代表任何类型和所有类型,所以使用这个函数的用户可能会传入一个 number
,而它并没有 .length
成员。
假设我们实际上想让这个函数处理 Type
数组,而不是直接处理 Type
。由于我们使用的是数组,所以 .length
成员应该是可用的。我们可以像创建其他类型的数组一样来描述:
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
你可以将 loggingIdentity
的类型解读为 “泛型函数 loggingIdentity
接收一个类型参数 Type
,以及一个参数 arg
,它是一个 Type
的数组,并返回一个 Type
的数组。” 如果传入一个数字数组,就会返回一个数字数组,因为 Type
会绑定到 number
。这允许我们使用泛型类型变量 Type
作为所处理类型的一部分,而不是整个类型,这给了我们更大的灵活性。
我们也可以用下面这种方式编写示例:
function loggingidentity<Type>(arg: Array<Type>): Array<Type> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
你可能已经在其他语言中熟悉了这种风格。在下一节中,我们将介绍如何创建自己的泛型类型,就像 Array<Type>
。
泛型类型
在前面几节中,我们创建了适用于一系列类型的泛型 identity 函数。在本节中,我们将探讨函数本身的类型以及如何创建泛型接口。
泛型函数的类型与非泛型函数的类型类似,类型参数放在最前面,类似于函数声明:
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;
我们还可以在类型中为泛型类型参数使用不同的名称,只要类型变量的数量和类型变量的使用方式一致即可。
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Input>(arg: Input) => Input = identity;
我们还可以将泛型类型写成对象字面量形式:
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: { <Type>(arg: Type): Type } = identity;
让我们开始编写第一个泛型接口。我们从前面的例子中获取对象字面量,并将其放置到一个接口中:
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
在类似的示例中,我们可能希望将泛型参数移动调整为整个接口的参数。这让我们一眼就可以看到泛型类型是什么(例如 Dictionary<string>
而不仅仅是 Dictionary
)。这使得类型参数对接口的所有其他成员可见。
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
注意,我们的示例发生了细微的变化。我们现在有一个非泛型函数签名,它是泛型类型的一部分,而不是描述泛型函数。当我们使用 GenericIdentityFn
时,现在还需要指定相应的类型参数(这里是:number
),从而有效地锁定底层调用签名将使用什么类型。理解什么时候把类型参数直接放在调用签名上,什么时候把它放在接口本身上,将有助于描述类型的哪些部位是泛型的。
除了泛型接口外,我们还可以创建泛型类。请注意,不能创建泛型枚举和泛型命名空间。
泛型类
泛型类具有与泛型接口类似的形式。泛型类在类名后面的尖括号(<>
)中有一个泛型类型参数列表。
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;
}
这是对 GenericNumber
类的字面用法,但你可能已经注意到,这里并没有限制它只能使用 number
类型。我们可以用字符串或更复杂的对象来代替。
let stringNumeric = new GenericNumber<string>();
stringNumberic.zeroValue = "";
stringNumberic.add = function(x, y) {
return x + y;
}
console.log(stringNumberic.add(stringNumberic.zeroValue, "test"));
就像使用接口一样,将类型参数放在类本身上可以确保类的所有属性都使用相同的类型。
正如我们在类一节中所介绍的,类的类型表现在两个方面:静态和实例。泛型类仅表现在其实例上,而非静态上,因此在使用类时,静态成员不能使用类的类型参数。
泛型约束
如果你还记得前面的例子,你可能想编写一个适用于对其功能有所了解的一组类型的泛型函数。在我们的 loggingIdentity
例子中,我们希望能够访问 arg
的 .length
属性,但是编译器不能证明每种类型都有 .length
属性,所以它警告我们不能做这样的假设。
function loggingIdentity<Type>(arg: Type): Type {
console.log(arg.length);
// Property 'length' does not exist on type 'Type'.
return arg;
}
我们不使用 any 类型和所有类型,而是将该函数限定在作用于具有 .length
属性的 any 类型和所有类型。只要类型有这个成员,我们就允许它,但它至少需要有这个成员。要做到这一点,我们必须列出我们的要求,作为对 Type
可以是哪种类型的约束。
为此,我们将创建一个描述约束的接口。这里,我们将创建一个具有单个 .length
属性的接口,然后我们将使用这个接口和 extends
关键字来表示我们的约束:
interface LengthWise {
length: number;
}
function loggingIdentity<Type extends LengthWise>(arg: Type): Type {
console.log(arg.length); // Now we know it has a .length property, so no more error.
return arg;
}
因为泛型函数现在受到了约束,它将不再适用于 any 类型和所有类型:
loggingIdentity(3);
// Argument of type 'number' is not assignable to parameter of type 'LengthWise'
相反,我们需要传入具有所有必需属性的类型的值:
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");
// Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
在泛型中使用类类型
在 TypeScript 中使用泛型创建工厂时,有必要通过类的构造函数引用类类型。例如:
function create<Type>(c: { new (): Type }): Type {
return new c();
}
一个更高级的例子是使用 prototype 属性来推断和约束 constructor 函数和类类型的实例之间的关系。
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag;
createInstance(Bee).keeper.hasMask;
该模式用于驱动 mixins 设计模式。
keyof 类型操作符
keyof
操作符接受一个对象类型并生成其键的字符串或数字字面量集合:
type Point = { x: number; y: number };
type p = keyof Point;
// ^ = type P = keyof Point
如果该类型具有 string
或 number
索引签名,keyof
将返回这些类型:
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
// ^ = type A = number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
// ^ = type M = string | number
注意,在这个例子中,M
是 string | number
-- 这是因为 JavaScript 对象的键总是强制转换为字符串,所以 obj[0]
总是和 obj["0"]
相同。
当与映射类型结合时,keyof
类型变得特别有用,稍后我们将了解更多。
typeof 类型操作符
JavaScript 已经有了可以在表达式上下文中使用的 typeof
操作符
// Prints "string"
console.log(typeof "Hello World");
TypeScript 添加了一个 typeof
操作符,你可以在类型上下文中使用它来引用变量或属性的类型:
let s = "hello";
let n: typeof s;
// ^ = let n: string
这对于基本类型不是很有用,但是结合其他类型操作符,你可以使用 typeof
方便地表示许多模式。例如,让我们从查看预定义类型 ReturnType<T>
开始。它接受一个函数类型并生成它的返回类型:
type Predicate = (x: number) => boolean;
type K = ReturnType<Predicate>;
// ^ = type K = boolean
如果试图在函数名上使用 ReturnType
,会出现一个指示性错误:
function f() {
return { x:10, y: 3 };
}
type P = ReturnType<f>;
// ‘f’ refers to a value, but is being used as a type here. Did you mean
// 'typeof f'?
记住,值和类型不是一回事。为了引用值 f 的类型,我们使用 typeof
:
function f() {
return { x:10, y: 3 };
}
type P = ReturnType<typeof f>;
// ^ = type P = {
// x: number;
// y: number;
// }
局限
TypeScript 有意限制了可以使用 typeof
的表达式类型。
具体来说,只有在标识符(即变量名)或它们的属性上使用 typeof
才是合法的。这有助于避免编写你认为正在执行但实际上并没有执行的代码陷阱:
// Meant to use = ReturnType<typeof msgbox>
let shouldContinue: typeof msgbox("Are you sure you want to continue?");
// ',' expected.
索引访问类型
我们可以使用索引访问类型来查找另一种类型上的特定属性:
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"];
// ^ = type Age = number
索引类型本身就是一种类型,所以我们完全可以使用 unions
、keyof
或其他类型
type I1 = Person["age" | "name"];
// ^ = type I1 = string | number
type I2 = Person[keyof Person];
// ^ = type I2 = string | number | boolean
type AliveOrName = "alive" | "name";
type I3 = Person[AliveOrName];
// ^ = type I3 = string | boolean
如果你试图索引一个不存在的属性,就会出现报错:
type I1 = Person["alve"];
// Property 'alve' does not exist on type 'Person'.
使用任意类型进行索引的另一个例子是使用 number
来获取数组元素的类型。我们可以将其与 typeof
相结合,方便地获取数组字面量的元素类型:
const MyArray = [
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
type Person = typeof MyArray[number];
// ^ = type Person = {
// name: string;
// age: number;
// }
type Age = typeof MyArray[number]["age"];
// ^ = type Age = number
// Or
type Age2 = Person["age"];
// ^ = type Age2 = number
条件类型
最实用的程序核心就是,我们必须根据输入做出决定。JavaScript 程序也不例外,但是考虑到值能够很容易地自查,这些决定也基于输入的类型。条件类型有助于描述输入和输出之间的关系。
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string;
// ^ = type Example1 = number
type Example2 = RegExp extends Animal ? number : string;
// ^ = type Example2 = string
条件类型的形式看起来有点像 JavaScript 中的条件表达式(condition ? trueExpression : falseExpression
):
SomeType extends OtherType ? TrueType : FalseType;
当 extends
左边的类型可赋值给右边的类型时,你就会得到第一个分支中的类型(“true”分支);否则将从后面的分支(“false”分支)中获得类型。
上面例子中的条件类型可能看起来用处不大 -- 我们可以告诉自己 Dog
是否扩展了 Animal
并选择 number
或 string
!但是条件类型的强大之处在于它与泛型一起使用。
例如,让我们使用下面的 createLabel
函数:
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLable {
throw "unimplemented";
}
createLabel
的这些重载描述了一个 JavaScript 函数,该函数根据其输入的类型做出选择。注意下面两点:
- 如果一个库必须在整个 API 中反复做出相同的选择,这就变得很麻烦。
- 我们必须创建三个重载:一个用于每种单独的情况(一个用于
string
,一个用于number
),一个用于最一般的情况(使用string | number
)。createLabel
每增加一种新的处理类型,重载的数量就会呈指数增长。
因此,我们将该逻辑改写为条件类型:
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 = createLabel("typescript");
// ^ = let a: NameLabel
let b = createLabel(2.8);
// ^ = let b: IdLabel
let c = createLabel(Math.random() ? "hello" : 42);
// ^ = let c = NameLabel | IdLabel
条件类型约束
通常,条件类型中的检查将为我们提供一些新信息。就像使用类型守卫收缩(narrowing with type guard)可以为我们提供更具体的类型一样,条件类型的 true 分支将通过我们所检查的类型进一步约束泛型。
type MessageOf<T> = T["message"];
// Type '"message"' cannot be used to index type 'T'.
在这个例子中,TypeScript 报错是因为 T
不知道有一个名为 message
的属性。我们可以约束 T
,这样 TypeScript 就不会再报错了:
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>;
// ^ = type EmailMessageContents = string
但是,如果我们想让 MessageOf
接受任何类型,如果 message
属性不可用,则默认为 never
,该如何处理?我们可以通过将约束移除并引入条件类型来实现这一点:
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMessageContents = MessageOf<Email>;
// ^ = type EmailMessageContents = string
type DogMessageContents = MessageOf<Dog>;
// ^ = type DogMessageContents = never
在 true
分支中,TypeScript 知道 T
将有一个 message
属性。
来看另一个例子,我们可以编写一个实现数组扁平化的 Flatten
类型:
type Flatten<T> = T extends any[] ? T[number] : T;
// Extracts out the element type
type str = Flatten<string[]>;
// ^ = type Str = string
// Leaves the type alone
type Num = Flatten<number>;
// ^ = type Num = number
当 Flatten
得到一个数组类型时,它使用带 number
的索引访问来获取 string[]
的元素类型。否则,它只返回给定的类型。
条件类型推断
我们刚才使用条件类型来应用约束,然后提取类型。这是一种非常常见的操作,条件类型使它变得更容易。
条件类型为我们提供了一种使用 infer
关键字从我们在 true
分支中的类型进行推断的方法。例如,我们可以在 Flatten
中推断元素类型,而不是使用索引访问类型”手动”取出元素类型:
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
这里,我们使用 infer
关键字声明性地引入了一个 新的泛型类型变量 Item
,而不是指定如何在 true 分支中检索 T
的元素类型。这样我们就不必考虑如何挖掘和探索我们感兴趣的类型的结构。
我们可以使用 infer
关键字编写一些有用的助手类型别名。例如,对于简单的情况,我们可以从函数类型中提取返回类型:
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return ? Return : never;
type Num = GetReturnType<() => number>;
// ^ = type Num = number
type Str = GetReturnType<(x: string) => string>;
// ^ = type Str = string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
// ^ = type Bools = boolean[]
当从具有多个调用签名的类型(例如重载函数的类型)进行推断时,从最后一个签名进行推断是最容易被广泛接受的。不可能基于参数类型列表执行重载解析。
declare function stringOrNum(x: string): numbet;
declare function stringOrNum(x: number): string;
declare function stringorNum(x: string | number): string | number;
type T1 = ReturnType<typeof stringOrNum>;
// ^ = type T1 = string | number
分布式条件类型
当条件类型作用于泛型类型并且给定联合类型时,它们就会成为分布式类型。例如,下面的例子:
type ToArray<Type> = Type extends any ? Type[] : never;
如果将一个联合类型插入到 ToArray
中,则条件类型将作用于该联合体的每个成员。
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
// ^ = type StrArrOrNumArr = string[] | number[]
这里的 StrArrOrNumArr
的分布式就表现在:
string | number;
并将联合体的每个成员类型映射成有效的:
ToArray<string> | ToArray<number>;
最终得到:
string[] | number[];
通常,分布式是预期行为。为了避免这种行为,可以用方括号将 extends
关键字的每一边都括起来。
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'strOrNumArr' is no longer a union.
type StrOrNumArr = ToArrayNonDist<string | number>;
// ^ = type StrOrNumArr = (string | number)[];
映射类型
当你不想重复自己的内容时,有时一个类型需要基于另一个类型。
映射类型建立在索引签名的语法之上,索引签名用于声明未提前声明的属性类型:
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
}
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
}
映射类型是一种泛型类型,它使用通过 keyof 创建的 union 来遍历每种类型的键以创建另一种类型:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
}
在这个例子中,OptionFlags
将从 Type
类型中获取所有属性,并将它们的值更改为布尔值。
type FeatureFlags = {
darkMode: () => void;
newUserProfile: () => void;
}
type FeatureOptions = OptionsFlags<FeatureFlags>;
// ^ = type FeatureOptions = {
// darkMode: boolean;
// newUserProfile: boolean;
// }
映射修饰词
有两个额外的修饰符可以在映射期间应用:readonly
和 ?
分别影响可变性和可选性。
你可以通过前缀 -
或 +
来删除或添加这些修饰符。如果不添加前缀,则假定为 +
。
// Removes 'readonly' attributes from a type's properties
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property];
};
type LockedAccount = {
readonly id: string;
readonly name: string;
}
type UnlockedAccount = CreateMutable<LockedAccount>;
// ^ = type UnlockedAccount = {
// id: string;
// name: string;
// }
// Removes 'optional' attributes from a type's properties
type Concrete<Type> = {
[Property in keyof Type]-?: Type[property];
}
type MaybeUser = {
id: string;
name?: string;
age?: number;
};
type User = Concrete<MaybeUser>;
// ^ = type User = {
// id: string;
// name: string;
// age: number;
// }
通过 as 重新映射键
在 TypeScript 4.1 及以后的版本中,你可以在映射类型中使用 as 子句来重新映射键:
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
你可以利用模板字面量类型等特性,从以前的属性名中创建新的属性名:
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
interface Person {
name: string;
age: number;
location: string;
}
type LazyPerson = Getters<Person>;
// ^ = type LazyPerson = {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
// }
你可以通过条件类型生成 never 来过滤键:
// Remove the 'kind' property
type RemoveKindField<Type> = {
[Property in keyof Type as Exclude<property, "kind">]: Type[Property]
};
interface Circle {
kind: "circle";
radius: number;
}
type KindlessCircle = RemoveKindField<Circle>;
// ^ = type KindlessCircle = {
// radius: number;
// }
进一步探索
映射类型与该类型操作部分中的其他特性可以很好地合作,例如,这里有一个使用条件类型的映射类型,它根据对象是否将属性 pii
设置为字面量 true
返回 true
或 false
:
type ExtractPII<Type> = {
[Property in keyof Type]: Type[Property] extends { pii: true } ? true : false;
};
type DBFields = {
id: { format: "incrementing" };
name: { type: string; pii: true };
};
type ObjecsNeedingGDPRDeletion = ExtractPII<DBFields>;
// ^ = type ObjectsNeedingGDPRDeletion = {
// id: false;
// name: true;
// }
模板字面类型
模板字面类型建立在 字符串字面类型的基础上,并且能够通过联合体扩展成许多字符串。
语法上与 JavaScript 中的模板字面量字符串相同,但用于类型位置。当与具体的字面类型一起使用时,模板字面量通过连接其内容产生一个新的字符串字面类型。
type World = "world";
type Greeting = `hello ${World}`;
// ^ = type Greeting = "hello world"
当在插入的位置使用联合体时,类型是每个联合成员可以表示的每个可能的字符串字面量的集合:
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// ^ = typ AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
对于模板字面量中的每个插入位置,联合体都交叉相乘:
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
type Lang = "en" | "ja" | "pt";
type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
// ^ = type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id"
// 共有 4*3 = 12 种,这里省略了部分
我们通常建议大家对大型的合并字符串使用提前生成的方式,但这在规模较小的情况下是有用的。
类型中的字符串联合体
模板字面量的强大之处在于根据类型中已有的字符串定义新的字符串。
例如,JavaScript 中的一种常见模式是根据对象当前拥有的字段扩展对象。我们将提供一个函数的类型定义,它支持 on 函数,让你知道某个值何时发生了变化:
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
});
person.on("firstNameChanged", (newValue) => {
console.log(`firstName was changed to ${newValue}!`);
});
注意,on 监听 "firstNameChanged"
事件,而不仅仅是 "firstName"
事件,模板字面量提供了一种方法来处理类型系统内的此类字符串操作:
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
}
// Create a "watched object" with an 'on' method
// so that you can watch for changes to properties.
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>
有了它,我们可以构建一些在给定错误属性时会报错的内容:
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", () => {});
// It's typo-resistent
person.on("firstName", () => {});
// Argument of type '"firstName"' is not assignable to paramter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
person.on("frstNameChanged", () => {});
// Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
带有模板字面量的推断
请注意,前面的示例没有重用原始值的类型。使用 any.
模板字面量类型的回调函数可以从替换位置推断。
我们可以让最后一个示例泛型化,从 eventName
字符串的某些部分推断出关联的属性。
type PropEventSource<Type> = {
on<Key extends string & keyof Type>(eventName: `${Key}Changed`, callback: (newValue: Type[Key]))
}
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", newName => {
// ^ = (parameter) newName: string
console.log(`new name is ${newName.toUpperCase()}`);
})
person.on("ageChanged", newAge => {
// ^ = (parameter) newAge: number
if (newAge < 0) {
console.warn("warning! negative age");
}
})
这里我们把它变成了一个泛型方法。
当用户使用字符串 "firstNameChanged"
调用时,TypeScript 会尝试推断 K
的正确类型。为此,它将根据 "Changed"
之前的内容匹配K
,并推断出字符串 "firstName"
。一旦 TypeScript 发现了这一点,on
方法就可以获取原始对象的 firstName
类型,在本例中是string
。类似地,当用 "ageChanged"
调用时,TypeScript 会找到 age
属性的类型 number
。
固有字符串操作类型
为了增强字符串操作,TypeScript 包含了一组可以用于字符串操作的类型。为了提高性能,这些类型内置在编译器中,在 .d.ts
文件中找不到它们。
Uppercase
将字符串中的每个字符转换为大写版本。
示例:
type Greeting = "Hello, world"
type ShoutyGreeting = Uppercase<Greeting>
// ^ = type ShoutyGreeting = "HELLO, WORLD"
type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
type MainID = ASCIICacheKey<"my_app">
// ^ = type MainID = "ID-MY_APP"
Lowercase
将字符串中的每个字符转换为同等的小写字母。
示例:
type Greeting = "Hello, world"
type QuietGreeting = Lowercase<Greeting>
// ^ = type QuietGreeting = "hello, world"
type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
type MainID = ASCIICacheKey<"MY_APP">
// ^ = type MainID = "id-my_app"
Capitalize
示例:
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
// ^ = type Greeting = "Hello, world"
Uncapitalize
示例:
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
// ^ = type UncomfortableGreeting = "hELLO WORLD"