TypeScript 的类型系统非常强大,因为它允许用其他类型来表示类型。这种理念最简单的形式就是泛型。此外,我们还可以使用各种各样的类型运算符。也可以用已有的值来表示类型。通过组合各种类型运算符,我们可以以简洁、易于维护的方式表达复杂的操作和值。
1. 泛型(Generics)
软件工程的一个重要组成部分是构建组件,这些组件不仅要拥有定义明确且一致的API,还要具备可重用性 。能够处理当前数据以及未来数据 的组件,将为构建大型软件系统提供最大的灵活性。在 C# 和 Java 等语言中,创建可重用组件的主要工具之一是泛型,即能够创建可处理多种类型而非单一类型的组件。这使得用户可以使用这些组件并应用他们自己的类型。
1.1 泛型函数:
- 常规表示:
typescript
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: <Type>(arg: Type) => Type = identity;
- 基于interface的定义:
typescript
interface GenericIdentityFn {
<Type>(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
或者:
typescript
interface GenericIdentityFn<Type> {
(arg: Type): Type;
}
function identity<Type>(arg: Type): Type {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
1.2 泛型类
- 常规定义:
typescript
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;
};
注: add 是函数类型属性,并非常规方法,所以类本身不需要提供实现。
- 我们也可以使用字符串,甚至更复杂的对象。
typescript
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
1.3 泛型约束
- 常规定义
typescript
interface Lengthwise {
length: number;
}
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
console.log(arg.length);
return arg;
}
- 你可以声明一个受另一个类型参数约束的类型参数。例如,这里我们想根据对象名称获取其属性。为了确保不会意外获取对象上不存在的属性,我们将在两个类型之间添加约束:
typescript
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"); // 错误
- 在 TypeScript 中使用泛型创建工厂时,必须通过构造函数来引用类类型。例如:
typescript
function create<Type>(c: { new (): Type }): Type {
return new c();
}
- 设置默认泛型参数
typescript
declare function create<T extends HTMLElement = HTMLDivElement, U extends HTMLElement[] = 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[]>
1.4 协变与逆变(covariance & covariance)
- 协变(covariance):我们可以使用
Producer<Cat>来代替预期的Producer<Animal>,因为猫属于动物。这种关系称为协方差:从 Producer<T>到Producer<U>的关系与从 T 到 U 的关系相同。
typescript
interface Producer<out T> {
make(): T;
}
- 逆变(covariance):如果可以在预期是
Consumer<Cat>的地方使用Consumer<Animal>,因为任何能够接受Animal的函数也必然能够接受Cat。这种关系称为逆变:从 Consumer<T>到Consumer<U>的关系与从U到T的关系相同。注意与协变相比,方向相反!这就是为什么逆变会自我抵消,而协变不会。
typescript
interface Consumer<in T> {
consume: (arg: T) => void;
}
2. Keyof 操作符
keyof 运算符接受一个对象类型,并生成其键的字符串或数值字面量联合
typescript
type Point = { x: number; y: number };
type P = keyof Point;
- 如果类型具有字符串或数字索引签名,keyof 将返回这些类型:
typescript
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
3. Typeof 操作符
TypeScript 添加了 typeof 运算符,您可以在类型上下文中使用它来引用变量或属性的类型 (不同于JavaScript的typeof 操作符)。
typescript
let s = "hello";
let n: typeof s; // let n: string
- 可以使用 typeof 来方便地表达许多模式。例如,我们先来看预定义的类型 ReturnType。它接受一个函数类型并生成其返回类型:
typescript
type Predicate = (x: unknown) => boolean;
type K = ReturnType<Predicate>; //type K = boolean
- ReturnType 中的 TFunction 是函数类型, 不是函数本身
typescript
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<f>; // 错误
应该换成
typescript
function f() {
return { x: 10, y: 3 };
}
type P = ReturnType<typeof f>;
// type P = {
// x: number;
// y: number;
// }
4. 利用索引访问类型
- 我们可以使用索引访问类型来查找另一种类型的某个属性的类型:
typescript
type Person = { age: number; name: string; alive: boolean };
type Age = Person["age"]; // type Age = number
- 索引类型本身也是一种类型,因此我们可以完全使用联合类型、
keyof类型或其他类型:
typescript
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
- 可以使用
number来获取数组元素的类型。我们可以将其与typeof结合使用,以便方便地获取数组字面量的元素类型:
typescript
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 来引用变量:
typescript
const key = "age";
type Age = Person[key]; // 错误
如下是正确的:
typescript
type key = "age";
type Age = Person[key];
注 : type Age2 = Person["age"] 中的"age" 是类型。
5. 条件类型(Conditional Types)
5.1 描述输入和输出类型之间的关系。
typescript
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 中的条件表达式(
条件 ? trueExpression : falseExpression)类似:
typescript
SomeType extends OtherType ? TrueType : FalseType;
- 条件类型的强大之处在于它们与泛型结合使用。
typescript
interface IdLabel {
id: number
}
interface NameLabel {
name: string
}
type NameOrId<T extends number | string> = T extends number
? IdLabel
: NameLabel;
- 我们可以利用这种条件类型,将重载函数简化为一个没有重载的单一函数。
typescript
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
- 更复杂的例子:
typescript
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>; // type EmailMessageContents = string
typescript
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
typescript
type Flatten<T> = T extends any[] ? T[number] : T;
type Str = Flatten<string[]>; // type Str = string
type Num = Flatten<number>; //type Num = number
5.2 在条件类型中进行推断
条件类型为我们提供了一种方法,可以使用 infer 关键字从我们在 true 分支中比较的类型进行推断。
typescript
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
注 : 我们使用 infer 关键字以声明的方式引入了一个名为 Item 的新泛型类型变量,而不是在 true 分支中指定如何检索 Type 的元素类型。这使我们无需考虑如何深入挖掘和探测我们感兴趣的类型的结构。
- 我们可以使用
infer关键字编写一些有用的辅助类型别名:
typescript
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[]
- 当从具有多个调用签名的类型(例如重载函数 的类型)进行类型推断时,推断基于最后一个签名(这通常是最宽松的兜底情况)。无法基于参数类型列表执行重载解析。
typescript
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
5.3 可分配的条件类型(Distributive Conditional Types)
当条件类型作用于泛型类型时,如果给定的是一个联合类型,则它们就具有分配律。
typescript
type ToArray<Type> = Type extends any ? Type[] : never;
type StrArrOrNumArr = ToArray<string | number>;
// type StrArrOrNumArr = string[] | number[]
- 通常情况下,分配特性是理想的行为。为了避免 这种特性,你可以用方括号将
extends关键字的每一侧括起来。
typescript
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
type ArrOfStrOrNum = ToArrayNonDist<string | number>;
// type ArrOfStrOrNum = (string | number)[]
6. 映射类型(Mapped Types)
Mapped Types 是 TypeScript 提供的一种强大工具,用来基于已有类型生成新类型。它的核心思想是:对某个类型的属性集合进行遍历,并在遍历过程中应用规则来生成新的属性类型。
- 映射类型建立在索引签名语法的基础上,索引签名用于声明尚未预先声明的属性类型:
typescript
type OnlyBoolsAndHorses = {
[key: string]: boolean | Horse;
};
const conforms: OnlyBoolsAndHorses = {
del: true,
rodney: false,
};
- 映射类型是一种通用类型,它使用
PropertyKey的联合(通常通过keyof创建)来遍历键以创建类型:
typescript
type OptionsFlags<Type> = {
[Property in keyof Type]: boolean;
};
type Features = {
darkMode: () => void;
newUserProfile: () => void;
};
type FeatureOptions = OptionsFlags<Features>;
// type FeatureOptions = {
// darkMode: boolean;
// newUserProfile: boolean;
// }
注 : keyof 操作符返回的是字面量类型的联合, Property 仅仅是一个占位符,可以使用任意字符代替。
6.1 映射修饰符
- 映射过程中还可以应用两个额外的修饰符:
readonly和?,它们分别影响可变性和可选性。您可以通过在前缀中添加-或+来移除 或添加 这些修饰符。如果您不添加前缀,则默认为+。
typescript
type CreateMutable<Type> = {
-readonly [Property in keyof Type]: Type[Property]; // 移除 readonly
};
type LockedAccount = {
readonly id: string;
readonly name: string;
};
type UnlockedAccount = CreateMutable<LockedAccount>;
// type UnlockedAccount = {
// id: string;
// name: string;
// }
typescript
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;
// }
6.2 通过 as 进行按键重映射
从 TypeScript 4.1 开始,您可以使用映射类型中的 as 子句重新映射映射类型中的键:
typescript
type MappedTypeWithNewProperties<Type> = {
[Properties in keyof Type as NewKeyType]: Type[Properties]
}
- 可以利用模板字面量类型等功能,根据已有的属性名称创建新的属性名称:
typescript
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;
// }
注 : string & Property 提出非字符串类型的字面量。
- 您可以通过条件类型生成
never来过滤掉某些键:
typescript
// 剔除 "kind" 字面量类型
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;
// }
- 你可以对任意联合进行映射,不仅限于字符串、数字或符号的联合,而是任何类型的联合:
typescript
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;
// }
注 : E是针对联合成员的遍历,所以E在这里代表的类型就是SquareEvent和CircleEvent,E["kind"]为kind属性的字面量类型。
- 映射类型可以很好地与此类型操作部分中的其他功能配合使用:
typescript
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;
// }
7. 模板字面类型(Template Literal Types)
模板字面量类型基于字符串字面量类型 构建,并且可以通过联合 扩展为多个字符串 。它们的语法与 JavaScript 中的模板字面量字符串相同,但用于类型位置。当与具体的字面量类型一起使用时,模板字面量会通过连接内容生成一个新的字符串字面量类型。
typescript
type World = "world";
type Greeting = `hello ${World}`; //type Greeting = "hello world"
- 当联合用于插值位置时,其类型是每个联合成员可以表示的所有可能的字符串字面量的集合:
typescript
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"
- 对于模板字面量中的每个插值位置,将并集进行交叉相乘:
typescript
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"
7.1 类型中的字符串联合
typescript
type PropEventSource<Type> = {
on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
const person = makeWatchedObject({
firstName: "Saoirse",
lastName: "Ronan",
age: 26
});
person.on("firstNameChanged", () => {}); // 正确
person.on("firstName", () => {}); // 错误
person.on("frstNameChanged", () => {}); // 错误(拼写错误)
7.2 使用模板字面量进行推断
typescript
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");
}
})
7.3 字符串操作类型
为了便于字符串操作,TypeScript 包含了一组可用于字符串操作的类型。这些类型内置于编译器中以提高性能,因此在 TypeScript 自带的 .d.ts 文件中找不到。
- Uppercase
typescript
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
typescript
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
typescript
type LowercaseGreeting = "hello, world";
type Greeting = Capitalize<LowercaseGreeting>;
// type Greeting = "Hello, world"
- Uncapitalize
typescript
type UppercaseGreeting = "HELLO WORLD";
type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
// type UncomfortableGreeting = "hELLO WORLD"