一份关于 TypeScript 强大的映射类型和条件类型的综合指南,包含实用示例和高级用例,用于创建健壮且类型安全的应用程序。
精通 TypeScript 的映射类型与条件类型
TypeScript 作为 JavaScript 的一个超集,提供了强大的功能来创建健壮且可维护的应用程序。在这些功能中,映射类型 (Mapped Types) 和 条件类型 (Conditional Types) 作为高级类型操作的基本工具脱颖而出。本指南全面概述了这些概念,探讨了它们的语法、实际应用和高级用例。无论您是经验丰富的 TypeScript 开发者还是刚刚起步,本文都将为您提供有效利用这些功能的知识。
什么是映射类型?
映射类型允许您通过转换现有类型来创建新类型。它们会遍历现有类型的属性,并对每个属性应用转换。这对于创建现有类型的变体特别有用,例如将所有属性变为可选或只读。
基本语法
映射类型的语法如下:
type NewType<T> = {
[K in keyof T]: Transformation;
};
T
: 您想要映射的输入类型。K in keyof T
: 遍历输入类型T
中的每个键。keyof T
创建了T
中所有属性名称的联合类型,而K
在迭代过程中代表每个单独的键。Transformation
: 您想对每个属性应用的转换。这可以是添加修饰符(如readonly
或?
)、更改类型,或其他任何操作。
实用示例
将属性设为只读
假设您有一个表示用户个人资料的接口:
interface UserProfile {
name: string;
age: number;
email: string;
}
您可以创建一个所有属性均为只读的新类型:
type ReadOnlyUserProfile = {
readonly [K in keyof UserProfile]: UserProfile[K];
};
现在,ReadOnlyUserProfile
将拥有与 UserProfile
相同的属性,但它们都将是只读的。
将属性设为可选
同样,您可以将所有属性设为可选:
type OptionalUserProfile = {
[K in keyof UserProfile]?: UserProfile[K];
};
OptionalUserProfile
将拥有 UserProfile
的所有属性,但每个属性都将是可选的。
修改属性类型
您还可以修改每个属性的类型。例如,您可以将所有属性都转换为字符串:
type StringifiedUserProfile = {
[K in keyof UserProfile]: string;
};
在这种情况下,StringifiedUserProfile
中的所有属性都将是 string
类型。
什么是条件类型?
条件类型允许您根据条件定义类型。它们提供了一种根据类型是否满足特定约束来表达类型关系的方式。这类似于 JavaScript 中的三元运算符,但作用于类型。
基本语法
条件类型的语法如下:
T extends U ? X : Y
T
: 被检查的类型。U
:T
所扩展的类型(即条件)。X
: 如果T
扩展了U
(条件为真),则返回的类型。Y
: 如果T
没有扩展U
(条件为假),则返回的类型。
实用示例
判断一个类型是否为字符串
让我们创建一个类型,如果输入类型是字符串,则返回 string
,否则返回 number
:
type StringOrNumber<T> = T extends string ? string : number;
type Result1 = StringOrNumber<string>; // string
type Result2 = StringOrNumber<number>; // number
type Result3 = StringOrNumber<boolean>; // number
从联合类型中提取类型
您可以使用条件类型从联合类型中提取特定类型。例如,提取非空类型:
type NonNullable<T> = T extends null | undefined ? never : T;
type Result4 = NonNullable<string | null | undefined>; // string
在这里,如果 T
是 null
或 undefined
,类型将变为 never
,然后 TypeScript 的联合类型简化会将其过滤掉。
推断类型
条件类型也可以与 infer
关键字一起使用来推断类型。这允许您从更复杂的类型结构中提取类型。
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function myFunction(x: number): string {
return x.toString();
}
type Result5 = ReturnType<typeof myFunction>; // string
在此示例中,ReturnType
提取了函数的返回类型。它检查 T
是否是一个接受任何参数并返回类型 R
的函数。如果是,它返回 R
;否则,它返回 any
。
结合映射类型和条件类型
映射类型和条件类型的真正威力在于将它们结合起来。这使您能够创建高度灵活和富有表现力的类型转换。
示例:深度只读 (Deep Readonly)
一个常见的用例是创建一个类型,使对象的所有属性(包括嵌套属性)都变为只读。这可以通过递归条件类型来实现。
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Company {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyCompany = DeepReadonly<Company>;
在这里,DeepReadonly
递归地将 readonly
修饰符应用于所有属性及其嵌套属性。如果一个属性是对象,它会递归地对该对象调用 DeepReadonly
。否则,它只是将 readonly
修饰符应用于该属性。
示例:按类型过滤属性
假设您想创建一个只包含特定类型属性的类型。您可以结合使用映射类型和条件类型来实现这一点。
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type StringProperties = FilterByType<Person, string>; // { name: string; }
type NonStringProperties = Omit<Person, keyof StringProperties>;
在此示例中,FilterByType
遍历 T
的属性,并检查每个属性的类型是否扩展了 U
。如果是,它就在结果类型中包含该属性;否则,它通过将键映射到 never
来排除它。注意使用 "as" 来重映射键。然后我们使用 `Omit` 和 `keyof StringProperties` 从原始接口中移除字符串属性。
高级用例和模式
除了基本示例外,映射类型和条件类型还可以用于更高级的场景,以创建高度可定制和类型安全的应用程序。
分布式条件类型
当被检查的类型是联合类型时,条件类型是分布式的。这意味着条件会分别应用于联合的每个成员,然后将结果组合成一个新的联合类型。
type ToArray<T> = T extends any ? T[] : never;
type Result6 = ToArray<string | number>; // string[] | number[]
在此示例中,ToArray
分别应用于联合类型 string | number
的每个成员,结果为 string[] | number[]
。如果条件不是分布式的,结果将是 (string | number)[]
。
使用工具类型
TypeScript 提供了几个内置的工具类型,它们利用了映射类型和条件类型。这些工具类型可以作为更复杂类型转换的构建块。
Partial<T>
: 使T
的所有属性变为可选。Required<T>
: 使T
的所有属性变为必需。Readonly<T>
: 使T
的所有属性变为只读。Pick<T, K>
: 从T
中选择一组属性K
。Omit<T, K>
: 从T
中移除一组属性K
。Record<K, T>
: 构造一个具有一组类型为T
的属性K
的类型。Exclude<T, U>
: 从T
中排除所有可分配给U
的类型。Extract<T, U>
: 从T
中提取所有可分配给U
的类型。NonNullable<T>
: 从T
中排除null
和undefined
。Parameters<T>
: 获取函数类型T
的参数。ReturnType<T>
: 获取函数类型T
的返回类型。InstanceType<T>
: 获取构造函数类型T
的实例类型。
这些工具类型是强大的工具,可以简化复杂的类型操作。例如,您可以结合使用 Pick
和 Partial
来创建一个只使某些属性可选的类型:
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: number;
name: string;
price: number;
description: string;
}
type OptionalDescriptionProduct = Optional<Product, "description">;
在此示例中,OptionalDescriptionProduct
拥有 Product
的所有属性,但 description
属性是可选的。
使用模板字面量类型
模板字面量类型允许您基于字符串字面量创建类型。它们可以与映射类型和条件类型结合使用,以创建动态和富有表现力的类型转换。例如,您可以创建一个类型,为所有属性名称添加特定前缀:
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${string & K}`]: T[K];
};
interface Settings {
apiUrl: string;
timeout: number;
}
type PrefixedSettings = Prefix<Settings, "data_">;
在此示例中,PrefixedSettings
将拥有属性 data_apiUrl
和 data_timeout
。
最佳实践与注意事项
- 保持简单: 尽管映射类型和条件类型功能强大,但它们也可能使您的代码变得更复杂。尽量保持您的类型转换尽可能简单。
- 使用工具类型: 尽可能利用 TypeScript 的内置工具类型。它们经过了充分的测试,可以简化您的代码。
- 为您的类型编写文档: 清晰地记录您的类型转换,特别是当它们很复杂时。这将帮助其他开发人员理解您的代码。
- 测试您的类型: 使用 TypeScript 的类型检查来确保您的类型转换按预期工作。您可以编写单元测试来验证类型的行为。
- 考虑性能: 复杂的类型转换可能会影响 TypeScript 编译器的性能。注意您的类型的复杂性,避免不必要的计算。
结论
映射类型 和 条件类型 是 TypeScript 中强大的功能,使您能够创建高度灵活和富有表现力的类型转换。通过掌握这些概念,您可以提高 TypeScript 应用程序的类型安全性、可维护性和整体质量。从简单的转换(如使属性可选或只读)到复杂的递归转换和条件逻辑,这些功能为您提供了构建健壮和可扩展应用程序所需的工具。不断探索和试验这些功能,以释放它们的全部潜力,成为一名更熟练的 TypeScript 开发人员。
在您继续 TypeScript 之旅时,请记得利用丰富的可用资源,包括官方 TypeScript 文档、在线社区和开源项目。拥抱映射类型和条件类型的力量,您将能够从容应对最棘手的类型相关问题。