TypeScript 中的高级类型包括映射类型、条件类型、字面量类型和递归类型等强大结构。这些特性使开发者能够表达类型之间更复杂的关系,从而处理边缘情况,并定义更动态、更灵活的类型系统。
一、映射类型
TypeScript 映射类型(Mapped Types)是一种高级类型工具,它允许我们基于已有的类型创建新的类型。通过遍历已有类型的键(key),并对其进行变换,可以快速构造具有相同结构但属性类型不同的新类型,从而提高代码的灵活性和复用性。
(一) 概念
TypeScript 中的映射类型(Mapped Types)允许你通过转换现有类型的属性来创建新的类型。
- 它们支持对属性进行修改,例如将属性设为可选(optional)、只读(read-only),或者改变属性的类型。
- 映射类型有助于减少重复代码,并通过自动化的类型转换提升类型安全性。
- 它们特别适用于创建现有类型的不同变体,而无需手动重新定义每个属性。
例如下面的代码所示:
type User = {id: number;name: string;email: string;
};type PartialUser = {[P in keyof User]?: User[P];
};
代码解释:
PartialUser
是一个新类型,其中User
类型的每个属性都被标记为可选(optional)。keyof
操作符用于获取User
类型的所有属性键,而映射类型则会遍历每一个键,并通过?
将它们标记为可选属性。
输出:
const user1: PartialUser = { id: 1 };
const user2: PartialUser = {};
const user3: PartialUser = { id: 2, name: "Alice" };
(二) 场景示例
1. 创建只读属性
在 TypeScript 中,可以使用 映射类型(Mapped Types) 和 readonly
关键字创建一个新的类型,使其所有属性变为只读。
type User = {id: number;name: string;email: string;
};type ReadonlyUser = {readonly [P in keyof User]: User[P];
};const user: ReadonlyUser = { id: 1, name: "Alice", email: "alice@example.com" };
user.id = 2;
代码解释:
- ReadonlyUser 是一个新类型,其中 User 的所有属性都被标记为只读(readonly)。
- 尝试修改 user 对象的任何属性都会导致编译时错误。
输出:
Error: Cannot assign to 'id' because it is a read-only property.
2. 创建可为空属性
创建可为空属性(Creating Nullable Properties)指的是将一个类型中的所有属性变为可以为 null
的类型。在 TypeScript 中,可以使用映射类型(Mapped Types)结合联合类型(Union Types)来实现这一点。
type Product = {name: string;price: number;inStock: boolean;
};type NullableProduct = {[P in keyof Product]: Product[P] | null;
};const product: NullableProduct = { name: "Laptop", price: null, inStock: true };
代码解释:
- NullableProduct 是一个新的类型,它使得 Product 类型中的每个属性都可以是其原始类型,或者是
null
。 - 这允许属性显式地具有
null
值,用于表示该属性当前没有值或值缺失的情况。
输出:
{ name: "Laptop", price: null, inStock: true }
3. 使用模板字面量重命名属性
使用模板字面量重命名属性(Renaming Properties with Template Literals)是 TypeScript 中映射类型的一种高级用法。它允许我们通过字符串模板语法在创建新类型时动态地改变属性名。
type Person = {firstName: string;lastName: string;
};type PrefixedPerson = {[P in keyof Person as `person${Capitalize<P>}`]: Person[P];
};const person: PrefixedPerson = { personFirstName: "Felix", personLastName: "Raink" };
代码解释:
- PrefixedPerson 创建了一个新类型,通过在 Person 类型的每个属性名前加上 "person" 前缀,并将原属性名首字母大写。
- 这个示例演示了如何结合模板字面量类型和 TypeScript 内置的 Capitalize 工具类型来转换属性名称。
输出:
{ personFirstName: "Felix", personLastName: "Raink" }
(三) TypeScript 映射类型使用最佳实践
- 保持转换简单:避免过于复杂的嵌套转换,以保持代码的可读性和维护的便捷性。
- 确保类型安全:利用映射类型强制执行一致的属性转换,提高整个代码库的类型安全性。
- 结合内置工具类型使用:配合 Partial、Readonly、Pick 和 Omit 等内置工具类型,简化常见的类型转换操作。
二、条件类型
在 TypeScript 中,条件类型使开发者能够根据条件创建类型,从而实现更动态和灵活的类型定义。
它们遵循语法 T extends U ? X : Y
,意思是如果类型 T
可赋值给类型 U
,则类型解析为 X
;否则解析为 Y
。
(一) 概念
条件类型在创建工具类型和进行高级类型操作时特别有用,能够增强代码的复用性和类型安全性。
比如下面这个例子:
type IsString<T> = T extends string ? 'Yes' : 'No';type Test1 = IsString<string>;
type Test2 = IsString<number>;console.log('Test1:', 'Yes');
console.log('Test2:', 'No');
- 类型别名
IsString
使用条件类型来判断类型T
是否继承自string
。 - 如果
T
可以赋值给string
,则结果为'Yes'
;否则结果为'No'
。 Test1
被评估为'Yes'
,因为string
继承自string
。Test2
被评估为'No'
,因为number
不继承自string
。
输出:
Test1: Yes
Test2: No
(二) 场景示例
1. 条件类型约束
条件类型约束允许在条件类型中对泛型类型进行约束,从而实现动态且精确的类型处理。
type CheckNum<T> = T extends number ? T : never;type NumbersOnly<T extends any[]> = {[K in keyof T]: CheckNum<T[K]>;
};const num: NumbersOnly<[4, 5, 6, 8]> = [4, 5, 6, 8];
const invalid: NumbersOnly<[4, 6, "7"]> = [4, 6, "7"];
代码解释:
CheckNum<T>
确保只保留数字类型;其他类型则解析为never
。NumbersOnly<T>
对数组中的每个元素应用CheckNum
,过滤掉非数字类型。
输出:
Type '"7"' is not assignable to type 'never'.
2. 条件类型中的类型推断
此特性允许在条件类型定义中提取并使用类型,从而实现精确的类型转换。
type ElementType<T> = T extends (infer U)[] ? U : never;const numbers: number[] = [1, 2, 3];
const element: ElementType<typeof numbers> = numbers[0];
const invalidElement: ElementType<typeof numbers> = "string";
代码解释:
- ElementType<T> 使用 infer 关键字从数组中提取元素类型。
- 变量 element 被正确推断为 number;尝试赋值为字符串则无效。
输出:
Type 'string' is not assignable to type 'number'.
3. 分布式条件类型
分布式条件类型(Distributive Conditional Types)是 TypeScript 条件类型的一种特性,当条件类型作用于联合类型时,会将条件应用到联合类型的每个成员上,然后将结果合并成新的联合类型。
type Colors = 'red' | 'blue' | 'green';type ColorClassMap = {red: 'danger';blue: 'primary';green: 'success';
};type MapColorsToClasses<T extends string> = T extends keyof ColorClassMap? { [K in T]: ColorClassMap[T] }: never;const redClass: MapColorsToClasses<Colors> = { red: 'danger' };
const invalidClass: MapColorsToClasses<Colors> = { yellow: 'warning' };
代码解释:
MapColorsToClasses<T>
会检查类型T
是否匹配ColorClassMap
中的某个键,并将其映射为对应的值。- 像
'yellow'
这样无效的颜色会被拒绝,因为它不在ColorClassMap
中定义。
输出:
Type '{ yellow: "warning"; }' is not assignable to type 'never'.
(三) TypeScript 条件类型的最佳实践
- 使用条件类型创建灵活且可复用的类型定义。
- 将条件类型与泛型结合使用,以增强适应性。
- 在复杂场景中利用
infer
关键字实现类型推断。
三、字面量类型
TypeScript 的字面量类型允许开发者为变量、函数参数或属性指定精确的值,通过确保变量只能持有预定义的值来增强类型安全性。
- 允许变量具有特定且精确的值。
- 通过限制允许的值范围,提高代码的可靠性。
以下是 TypeScript 中字面量类型的几种类型:
(一) 字符串字面量类型
字符串字面量类型允许变量只接受特定的一组字符串值。
type Direction = "Up" | "Down" | "Left" | "Right";let move: Direction;move = "Up"; // 有效赋值
// move = "Forward"; // 错误:类型 '"Forward"' 不能赋值给类型 'Direction'
- Direction 类型只能是指定的字符串字面量之一:“Up”、“Down”、“Left”或“Right”。
- 赋值为该集合之外的任何值都会导致编译时错误。
(二) 数字字面量类型
数字字面量类型限制变量只能取特定的一组数值。
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;function rollDice(): DiceRoll {return 4; // 有效的返回值// return 7; // 错误:类型 '7' 不能赋值给类型 'DiceRoll'
}
- DiceRoll 类型只能是 1 到 6 之间的数字之一。
- 返回任何不在该范围内的值都会导致编译错误。
(三) 布尔字面量类型
布尔字面量类型限制变量只能是布尔值 true 或 false。
type Success = true;function operation(): Success {return true; // 合法的返回值// return false; // 错误:类型 'false' 不能赋值给类型 'true'
}
- Success 类型严格限定为 true,返回 false 会导致编译时错误。
(四) TypeScript 字面量类型的最佳实践
- 使用字面量类型指定精确值:定义变量时使用字面量类型,将其限制为特定的预设值,从而提升代码的可预测性。
- 结合联合类型使用:利用联合类型让变量能接受有限的多个字面量值,增强类型安全性。
- 利用类型别名:为复杂的字面量类型组合创建类型别名,简化代码结构并提升可读性。
四、模板字面量类型
TypeScript 中的模板字面量类型允许通过使用模板字面量语法,将已有的字符串字面量类型组合,构造出新的字符串字面量类型。
(一) 概念
它们支持通过在模板字符串中嵌入联合类型或其他字面量类型,创建复杂的字符串模式。
该特性提升了类型安全性,使开发者可以在类型层面定义并强制执行特定的字符串格式。
示例代码:
type Size = "small" | "medium" | "large";
type SizeMessage = `The selected size is ${Size}.`;let message: SizeMessage;message = "The selected size is small."; // 有效
message = "The selected size is extra-large."; // 报错
代码解释:
Size
是一个联合类型,表示可能的尺寸值。SizeMessage
是一个模板字面量类型,通过嵌入Size
,构造出具体的字符串模式。- 变量
message
只能被赋值为符合SizeMessage
模式的字符串。
错误信息示例:
Type '"The selected size is extra-large."' is not assignable to type 'SizeMessage'.
(二) 场景示例
1. 使用 TypeScript 字面量定义路径
type ApiEndpoints = "users" | "posts" | "comments";
type ApiPath = `/api/${ApiEndpoints}`;const userPath: ApiPath = "/api/users";
const invalidPath: ApiPath = "/api/unknown";
ApiEndpoints
是一个联合类型,表示可能的 API 端点名称。ApiPath
是一个模板字面量类型,动态构造出以/api/
开头,后接ApiEndpoints
中任意值的字符串模式。userPath
是有效的,因为它符合构造的模式;而invalidPath
会报错。
错误信息示例:
Type '"/api/unknown"' is not assignable to type 'ApiPath'.
2. 使用模板字面量格式化消息
type Status = "success" | "error" | "loading";
type StatusMessage = `The operation is ${Status}.`;const successMessage: StatusMessage = "The operation is success.";
const invalidMessage: StatusMessage = "The operation is pending.";
Status
是一个联合类型,表示操作的可能状态。StatusMessage
构造字符串模式,用于描述操作状态。successMessage
是有效的,因为它符合模式;而invalidMessage
报错,因为"pending"
不属于Status
类型。
错误信息示例:
Type '"The operation is pending."' is not assignable to type 'StatusMessage'.
五、递归类型
TypeScript 为 JavaScript 添加了强类型支持。递归类型定义了可以自我引用的类型,适用于树形结构或嵌套对象。实用工具类型(Utility Types)则简化了类型的修改,比如将属性设为可选或只读。这些工具帮助我们写出更清晰、更灵活的代码。
(一) TypeScript 中的递归类型
递归类型是在其定义中引用自身的类型。这使得我们能够建模复杂的数据结构,比如树、链表和嵌套对象,其中类型可以嵌套自身。
- 允许定义自我引用的类型。
- 适合表示层级或嵌套数据。
- 必须使用 TypeScript 的类型别名(type alias)或接口(interface)来定义递归结构。
1. 语法示例
下面是一个表示树结构的简单递归类型:
type TreeNode = {value: number;children?: TreeNode[];
};
TreeNode
类型包含一个value
属性和一个可选的children
属性。children
是一个TreeNode
数组,支持嵌套结构。
2. 使用递归类型示例
const tree: TreeNode = {value: 1,children: [{ value: 2 },{value: 3,children: [{ value: 4 },{ value: 5 }]}]
};
在此示例中,tree
表示一个层级结构,节点可以有子节点,子节点又可以有自己的子节点,依此类推。
3. 递归类型的优点
- 建模复杂结构:递归类型方便表示层级结构。
- 类型安全:TypeScript 确保递归结构在每个嵌套层级都符合正确的类型。
- 类型复用:递归类型可以在多种场景中复用相同的结构定义。
(二) TypeScript 中的实用工具类型(Utility Types)
TypeScript 内置了一些实用工具类型,提供了修改或转换其他类型的现成功能,简化常见的类型操作,提高开发效率。
1. 常见实用工具类型
Partial<T>
将类型 T 的所有属性设为可选。
适用于创建部分属性可缺失的对象。
type Partial<T> = {[P in keyof T]?: T[P];
};
Required<T>
将类型 T 的所有属性设为必需。
确保对象中的所有属性必须存在。
type Required<T> = {[P in keyof T]?: T[P];
};
Readonly<T>
将类型 T 的所有属性设为只读。
防止初始化后修改对象属性。
type Readonly<T> = {readonly [P in keyof T]: T[P];
};
Pick<T, K>
从类型 T 中挑选出属性 K 的子集。
适用于从复杂类型中选取部分属性。
type Pick<T, K extends keyof T> = {[P in K]: T[P];
};
Omit<T, K>
从类型 T 中剔除属性 K。
用于排除不需要的属性。
type Omit<T, K extends keyof T> = {[P in Exclude<keyof T, K>]: T[P];
};
Record<K, T>
构造一个对象类型,其属性键为 K,值为 T。
用于定义类似映射的数据结构。
type Record<K extends keyof any, T> = {[P in K]: T;
};
Exclude<T, U>
从类型 T 中排除可赋值给 U 的类型。
用于过滤联合类型中的部分类型。
type Exclude<T, U> = T extends U ? never : T;
Extract<T, U>
从类型 T 中提取可赋值给 U 的类型。
用于缩小联合类型范围。
type Extract<T, U> = T extends U ? T : never;
NonNullable<T>
从类型 T 中排除 null
和 undefined
。
确保值不是 null 或 undefined。
type NonNullable<T> = T extends null | undefined ? never : T;
2. 实用工具类型的优点
- 简化类型转换:内置的工具类型方便对类型结构进行变换(如变成可选、必需等)。
- 提升代码可读性:使用简洁的类型关键字表达转换意图,使代码更清晰。
- 提高开发效率:避免重复手写复杂类型定义,减少错误。
通过递归类型和实用工具类型,TypeScript 为复杂数据结构和类型操作提供了强大而灵活的支持,助力开发者编写更安全、更高效的代码。