文章目录
Typescrtpt 类型操作
从类型创建类型
TypeScript 的类型系统非常强大,因为它允许用其他类型来表达类型。
- 这个想法的最简单形式是泛型。
- 此外,我们还有各种各样的类型运算符可供使用。
- 也可以用我们已经拥有的值来表达类型。
通过组合各种类型的操作符,我们可以用简洁、可维护的方式表达复杂的操作和值。下面我们将介绍根据现有类型或值来表达新类型的方法。
泛型
学习该节内容,需要有函数类型表达式、函数签名、构造签名等前置知识储备。
示例,用泛型创建一个恒等函数,即原封不动返回接收的参数。
如果没有泛型,我们要么必须给标识函数一个特定的类型,number 或者 string 等等,更合适的 any 类型接收任意类型参数,但 any 不能保证接收参数和返回参数是同一个类型。
function identity(arg: number): number {
return arg;
}
function identity(arg: any): any{
return arg;
}
基于此,引入一个特殊类型变量,它作用于类型而不是值。
function identity<T>(arg: T): T{
return arg;
}
一旦我们编写了泛型恒等函数,我们就可以通过两种方式之一调用它。第一种方法是将所有参数(包括类型参数)传递给函数。第二种方式也许也是最常见的。这里我们使用类型参数推断 - 也就是说,我们希望编译器根据我们传入的参数类型自动为我们设置 Type 的值。
// 第一种方式,显示传递 T 类型参数
let output = identity<string>("myString");
// 第二种,通过传递的 arg 参数自动推断出类型,赋值给 T
let output = identity("myString");
使用泛型类型变量
因为泛型 T 可以是任意类型,假设当我们想每次调用恒等函数时,将参数的 length 打印在控制台上,即认为 T 都是数组类型数据,那么可以将 Type 类型作为变量,创建一个 Type 类型的数组类型去做参数类型声明。
这样如果传入一个字符串数组,那么返回字符串数组。如果传入一个数字数组,那么返回数字数组。
function loggingIdentity<Type>(arg: Type[]): Type[] {
console.log(arg.length);
return arg;
}
泛型类型
前面介绍了利用泛型创建恒等函数。现在回归到函数本身的类型和如何创建泛型接口。
泛型函数的类型和非泛型函数的类型一样,类型参数先列出,类似于函数声明。其中泛型名称可以自定义。
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: { <Type>(arg: Type): Type } = identity;
即由此引入第一个泛型函数接口
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
但在类似的示例中,我们可能希望将泛型参数移动为整个接口的参数。这让我们可以看到我们是泛型的类型(例如 Dictionary 而不仅仅是 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;
};
泛型约束
有时你可能想要编写一个适用于一组类型的泛型函数,你知道该组类型将具有哪些功能。
例如,在前面的泛型类型变量的 loggingIdentity 示例中,我们希望能够访问 arg 的 .length 属性,但编译器无法证明每种类型都有 .length 属性,所以它警告我们不能做出这个假设。为此,我们必须将我们的要求列为对 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;
}
loggingIdentity(3); // 报错 Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
loggingIdentity({ length: 10, value: 3 }); // 正确
在泛型约束中使用类型参数
你可以声明受另一个类型参数约束的类型参数。
例如,在这里我们想从一个给定名称的对象中获取一个属性。We’我想确保我们’ 不会意外获取 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 中使用泛型创建工厂函数时,需要通过其构造函数引用类类型。例如,
其中 { new (): Type } 是对象字面量的构造签名。
// 注意此处用的是对象字面量写法,所以用的 : 符号
function create<Type>(c: { new (): Type }): Type {
return new c();
}
一个更高级的示例使用原型属性来推断和约束构造函数和类类型的实例端之间的关系。
class BeeKeeper {
hasMask: boolean = true;
}
class ZooKeeper {
nametag: string = "Mikle";
}
class Animal {
numLegs: number = 4;
}
class Bee extends Animal {
numLegs = 6;
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;
此模式用于为 混入 设计模式提供动力。
泛型参数默认值
通过声明泛型类型参数的默认值,你可以选择指定相应的类型参数。例如,创建新 HTMLElement 的函数。调用不带参数的函数会生成 HTMLDivElement;使用元素作为第一个参数调用函数会生成参数类型的元素。
declare function create<T extends HTMLElement = HTMLDivElement, U = T[]>(
element?: T,
children?: U
): Container<T, U>;
const div = create();
即
const div: Container<HTMLDivElement, HTMLDivElement[]>
const p = create(new HTMLParagraphElement());
即
const p: Container<HTMLParagraphElement, HTMLParagraphElement[]>
泛型参数默认值遵循以下规则:
-
如果一个类型参数有一个默认值,它就被认为是可选的。
-
必需的类型参数不能跟在可选的类型参数之后。
-
类型参数的默认类型必须满足类型参数的约束(如果存在)。
-
指定类型参数时,只需为需要的类型参数指定类型参数即可。未指定的类型参数将解析为其默认类型。
-
如果指定了默认类型并且推断无法选择候选者,则推断默认类型。
-
与现有类或接口声明合并的类或接口声明可能会为现有类型参数引入默认值。
-
与现有类或接口声明合并的类或接口声明可以引入新的类型参数,只要它指定默认值即可。
keyof 类型运算符
学习该节内容,需要有对象的索引签名等前置知识储备。
keyof 运算符采用对象类型并生成其键的字符串或数字字面联合。以下类型 P 与 type P = “x” | “y” 类型相同。
type Point = { x: number; y: number };
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”] 相同。
❗❗❗注意索引签名,如果限制了 number 类型,那么只能 number 类型索引,如果限制了 string 类型,则是 number | string 的联合类型,因为 Javascript 会自动将 number 类型索引转换为 string,所以相当于即支持 number 也支持 string。
keyof 类型在与映射类型结合使用时变得特别有用,我们稍后会详细了解。
typeof 类型运算符
学习该节内容,需要有 预定义类型 ReturnType 等前置知识储备。
JavaScript 已经有一个可以在表达式上下文中使用的 typeof 运算符:
console.log(typeof "Hello world"); // "string"
TypeScript 添加了一个 typeof 运算符,你可以在类型上下文中使用它来引用变量或属性的类型。
let s = "hello";
let n: typeof s; // 等价于 let n: string
由上可以看出,typeof 对于基本类型不是很有用,但结合其他类型运算符,你可以使用 typeof 方便地表达许多模式。
例如,让我们从查看预定义类型 ReturnType 开始。它接受一个函数类型并产生它的返回类型:
type Predicate = (x: unknown) => 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 是唯一合法的,这有助于避免与 Javascript 中 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
索引类型本身就是一种类型,所以我们可以完全使用联合、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
限制
你只能在索引时使用类型,这意味着你不能使用 const 来进行变量引用:
const key = "age";
type Age = Person[key];
// Type 'key' cannot be used as an index type.
// 'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?
但是,你可以将类型别名用于类似样式的重构。因为 key 是 Type。
type key = "age";
type Age = Person[key];
条件类型
学习本节内容,需要有 Typescript 关于函数 - 函数重载 的前置知识储备。
条件类型的形式看起来有点像 JavaScript 中的条件表达式 (condition ? trueExpression : falseExpression)。
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
从上面的示例来看,条件类型可能不会立即显得有用。条件类型的强大之处在于将它们与泛型一起使用。
示例,构造一个类型,可以接收 id、name、id | name 三种情况的参数。
用重载的写法如下,代码量大。
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 | NameLabel {
throw "unimplemented";
}
用泛型改造重载函数,使其不需要重载就能实现限制。写法如下
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 = createLabel("typescript");
// let a: NameLabel
let b = createLabel(2.8);
// let b: IdLabel
let c = createLabel(Math.random() ? "hello" : 42);
// let c: NameLabel | IdLabel
条件类型约束
通常,条件类型的检查会为我们提供一些新信息。就像使用类型保护进行缩小可以为我们提供更具体的类型一样,条件类型的真正分支将通过我们检查的类型进一步限制泛型。
例如,让我们采取以下措施:
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;
}
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 中的元素类型,而不是使用索引访问类型从 “manually” 中获取它:
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
在这里,我们使用 infer 关键字声明性地引入了一个名为 Item 的新泛型类型变量,而不是指定如何在 true 分支中检索 Type 的元素类型。这使我们不必考虑如何挖掘和探索我们感兴趣的类型的结构。
我们可以使用 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): number;
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[]
为避免这种行为,你可以用方括号将 extends 关键字的每一侧括起来,这样泛型传递的联合类型将作为一个整体,而不会将条件类型应用于联合的每个成员。
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
// 'ArrOfStrOrNum' is no longer a union.
type ArrOfStrOrNum = ToArrayNonDist<string | number>;
// type ArrOfStrOrNum = (string | number)[]
映射类型
映射类型建立在索引签名的语法之上,用于声明未提前声明的属性类型:
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
};
映射类型是一种泛型类型,它使用 PropertyKey 的联合(通过 keyof)来迭代键创建类型:
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
type Features = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<Features>;
// type FeatureOptions = {
// darkMode: boolean;
// newUserProfile: boolean;
// }
映射修饰符
在映射期间可以应用两个额外的修饰符:readonly 和 ? 分别影响可变性和可选性。
你可以通过添加前缀 - 或 + 来移除或添加这些修饰符。如果你不添加前缀,则假定为 +。意思是如果映射的类型属性有 readonly 修饰符,可以通过 - 删除映射过来的 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]
}
其中 NewKeyType 可以利用 模板字面类型等等功能从以前的属性名称中创建新的属性名称。
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;
// }
你可以映射任意联合,不仅是 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 = EventConfig<SquareEvent | CircleEvent>
// type Config = {
// square: (event: SquareEvent) => void;
// circle: (event: CircleEvent) => void;
// }
与其他类型操作配合
映射类型与此类型操作部分中的其他功能配合得很好,例如这里是使用条件类型的映射类型,它返回 true 或 false,具体取决于对象是否将属性 pii 设置为字面 true:
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 ObjectsNeedingGDPRDeletion = 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`;
// type 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" | "en_footer_title_id" | "en_footer_sendoff_id" | "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" | "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id"
我们通常建议人们对大型字符串联合使用提前生成,但这在较小的情况下很有用。
类型中的字符串联合
当基于类型内的信息定义一个新字符串时,模板字面的力量就来了。
示例,实现一个对象监听函数,当 on 方法被调用时,打印日志。
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26,
});
// makeWatchedObject has added `on` to the anonymous Object
person.on("firstNameChanged", (newValue) => {
console.log(`firstName was changed to ${newValue}!`);
});
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
// 正确
person.on("firstNameChanged", () => {});
person.on("firstName", () => {});
// 报错 Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
使用模板字符串进行推断
将上面例子的事件类型用泛型改写,从而监听所有属性的变化。这样回调函数返回值类型就不能是 any,而是实际属性的值类型。
type PropEventSource<Type> = {
on<Key extends string & keyof Type>
(eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
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");
}
})
内在字符串操作类型
为了帮助进行字符串操作,TypeScript 包含一组可用于字符串操作的类型。这些类型内置在编译器中以提高性能,在 TypeScript 附带的 .d.ts 文件中找不到。
详见 Typescript 工具类型