通过高级条件类型和映射类型释放 TypeScript 的强大功能。学习创建灵活、类型安全的应用,以适应复杂的数据结构。掌握编写真正动态 TypeScript 代码的艺术。
高级 TypeScript 模式:条件类型和映射类型精通
TypeScript 的强大之处在于它能够提供强类型,允许您及早发现错误并编写更易于维护的代码。 虽然 string
、number
和 boolean
等基本类型是基础,但 TypeScript 的高级特性(如条件类型和映射类型)开启了新的灵活性和类型安全维度。 本综合指南将深入探讨这些强大的概念,为您提供创建真正动态和可适应的 TypeScript 应用程序的知识。
什么是条件类型?
条件类型允许您定义依赖于条件的类型,类似于 JavaScript 中的三元运算符 (condition ? trueValue : falseValue
)。 它们使您能够根据类型是否满足特定约束来表达复杂的类型关系。
语法
条件类型的基本语法是:
T extends U ? X : Y
T
:要检查的类型。U
:要检查的类型。extends
:指示子类型关系的关键字。X
:如果T
可分配给U
,则使用的类型。Y
:如果T
无法分配给U
,则使用的类型。
本质上,如果 T extends U
评估为 true,则类型解析为 X
;否则,它解析为 Y
。
实际例子
1. 确定函数参数的类型
假设您想创建一个类型来确定函数参数是字符串还是数字:
type ParamType<T> = T extends string ? string : number;
function processValue(value: ParamType<string | number>): void {
if (typeof value === "string") {
console.log("Value is a string:", value);
} else {
console.log("Value is a number:", value);
}
}
processValue("hello"); // Output: Value is a string: hello
processValue(123); // Output: Value is a number: 123
在此示例中,ParamType<T>
是一个条件类型。 如果 T
是字符串,则类型解析为 string
;否则,它解析为 number
。 processValue
函数根据此条件类型接受字符串或数字。
2. 根据输入类型提取返回类型
想象一下,您有一个函数,它根据输入返回不同的类型。 条件类型可以帮助您定义正确的返回类型:
interface StringProcessor {
process(input: string): number;
}
interface NumberProcessor {
process(input: number): string;
}
type Processor<T> = T extends string ? StringProcessor : NumberProcessor;
function createProcessor<T extends string | number>(input: T): Processor<T> {
if (typeof input === "string") {
return { process: (input: string) => input.length } as Processor<T>;
} else {
return { process: (input: number) => input.toString() } as Processor<T>;
}
}
const stringProcessor = createProcessor("example");
const numberProcessor = createProcessor(42);
console.log(stringProcessor.process("example")); // Output: 7
console.log(numberProcessor.process(42)); // Output: "42"
在这里,Processor<T>
类型根据输入的类型有条件地选择 StringProcessor
或 NumberProcessor
。 这确保了 createProcessor
函数返回正确的处理器对象类型。
3. 区分联合类型
在使用区分联合时,条件类型非常强大。 区分联合是一种联合类型,其中每个成员都具有一个公共的单例类型属性(判别式)。 这允许您根据该属性的值缩小类型范围。
interface Square {
kind: "square";
size: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Circle;
type Area<T extends Shape> = T extends { kind: "square" } ? number : string;
function calculateArea(shape: Shape): Area<typeof shape> {
if (shape.kind === "square") {
return shape.size * shape.size;
} else {
return Math.PI * shape.radius * shape.radius;
}
}
const mySquare: Square = { kind: "square", size: 5 };
const myCircle: Circle = { kind: "circle", radius: 3 };
console.log(calculateArea(mySquare)); // Output: 25
console.log(calculateArea(myCircle)); // Output: 28.274333882308138
在此示例中,Shape
类型是区分联合。 Area<T>
类型使用条件类型来确定形状是正方形还是圆形,为正方形返回 number
,为圆形返回 string
(尽管在现实世界中,您可能希望一致的返回类型,但这演示了该原则)。
关于条件类型的主要内容
- 允许根据条件定义类型。
- 通过表达复杂的类型关系来提高类型安全性。
- 对于处理函数参数、返回类型和区分联合很有用。
什么是映射类型?
映射类型提供了一种通过映射其属性来转换现有类型的方法。 它们允许您基于另一种类型的属性创建新类型,应用修改,例如使属性可选、只读或更改其类型。
语法
映射类型的一般语法是:
type NewType<T> = {
[K in keyof T]: ModifiedType;
};
T
:输入类型。keyof T
:一个类型运算符,它返回T
中所有属性键的联合。K in keyof T
:迭代keyof T
中的每个键,将每个键分配给类型变量K
。ModifiedType
:将映射每个属性的类型。 这可以包括条件类型或其他类型转换。
实际例子
1. 使属性可选
您可以使用映射类型使现有类型的所有属性都变为可选:
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = {
[K in keyof User]?: User[K];
};
const partialUser: PartialUser = {
name: "John Doe",
}; // Valid, as 'id' and 'email' are optional
在这里,PartialUser
是一个映射类型,它迭代 User
接口的键。 对于每个键 K
,它通过添加 ?
修饰符使属性变为可选。 User[K]
检索 User
接口中属性 K
的类型。
2. 使属性只读
同样,您可以使现有类型的所有属性都变为只读:
interface Product {
id: number;
name: string;
price: number;
}
type ReadonlyProduct = {
readonly [K in keyof Product]: Product[K];
};
const readonlyProduct: ReadonlyProduct = {
id: 123,
name: "Example Product",
price: 25.00,
};
// readonlyProduct.price = 30.00; // Error: Cannot assign to 'price' because it is a read-only property.
在这种情况下,ReadonlyProduct
是一个映射类型,它将 readonly
修饰符添加到 Product
接口的每个属性。
3. 转换属性类型
映射类型也可用于转换属性的类型。 例如,您可以创建一个将所有字符串属性转换为数字的类型:
interface Config {
apiUrl: string;
timeout: string;
maxRetries: number;
}
type NumericConfig = {
[K in keyof Config]: Config[K] extends string ? number : Config[K];
};
const numericConfig: NumericConfig = {
apiUrl: 123, // Must be a number due to the mapping
timeout: 456, // Must be a number due to the mapping
maxRetries: 3,
};
此示例演示了在映射类型中使用条件类型。 对于每个属性 K
,它会检查 Config[K]
的类型是否为字符串。 如果是,则类型映射到 number
;否则,它保持不变。
4. 键重映射(自 TypeScript 4.1 起)
TypeScript 4.1 引入了使用 as
关键字在映射类型中重新映射键的功能。 这允许您基于原始类型创建具有不同属性名称的新类型。
interface Event {
eventId: string;
eventName: string;
eventDate: Date;
}
type TransformedEvent = {
[K in keyof Event as `new${Capitalize<string&K>}`]: Event[K];
};
// Result:
// {
// newEventId: string;
// newEventName: string;
// newEventDate: Date;
// }
//Capitalize function used to Capitalize first letter
type Capitalize<S extends string> = Uppercase<string&S> extends never ? string : `$Capitalize<S>`;
//Usage with an actual object
const myEvent: TransformedEvent = {
newEventId: "123",
newEventName: "New Name",
newEventDate: new Date()
};
在这里,TransformedEvent
类型将每个键 K
重新映射到一个新的键,该键以“new”为前缀并大写。 `Capitalize` 实用函数确保键的首字母大写。 `string & K` 交叉确保我们只处理字符串键,并且我们从 K 获取正确的字面量类型。
键重映射为将类型转换为特定需求打开了强大的可能性。 这允许您基于复杂的逻辑重命名、过滤或修改键。
关于映射类型的主要内容
- 通过映射其属性来转换现有类型。
- 允许使属性可选、只读或更改其类型。
- 对于基于另一种类型的属性创建新类型很有用。
- 键重映射(在 TypeScript 4.1 中引入)在类型转换方面提供了更大的灵活性。
结合条件类型和映射类型
当您将条件类型和映射类型结合使用时,才会发挥它们真正的威力。 这允许您创建高度灵活且具有表现力的类型定义,这些定义可以适应广泛的场景。
示例:按类型过滤属性
假设您想创建一个类型,该类型根据其类型过滤对象的属性。 例如,您可能只想从对象中提取字符串属性。
interface Data {
name: string;
age: number;
city: string;
country: string;
isEmployed: boolean;
}
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type StringData = StringProperties<Data>;
// Result:
// {
// name: string;
// city: string;
// country: string;
// }
const stringData: StringData = {
name: "John",
city: "New York",
country: "USA",
};
在此示例中,StringProperties<T>
类型使用带有键重映射和条件类型的映射类型。 对于每个属性 K
,它会检查 T[K]
的类型是否为字符串。 如果是,则保留该键;否则,它映射到 never
,从而有效地将其过滤掉。 never
作为映射类型键会将其从生成的类型中删除。 这可确保仅字符串属性包含在 StringData
类型中。
TypeScript 中的工具类型
TypeScript 提供了几个内置的工具类型,它们利用条件类型和映射类型来执行常见的类型转换。 了解这些工具类型可以大大简化您的代码并提高类型安全性。
常见工具类型
Partial<T>
:使T
的所有属性变为可选。Readonly<T>
:使T
的所有属性变为只读。Required<T>
:使T
的所有属性成为必需的。 (删除?
修饰符)Pick<T, K extends keyof T>
:从T
中选择一组属性K
。Omit<T, K extends keyof T>
:从T
中删除一组属性K
。Record<K extends keyof any, 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
的实例类型。ThisType<T>
:用作上下文this
类型的标记。
这些工具类型是使用条件类型和映射类型构建的,展示了这些高级 TypeScript 特性的强大功能和灵活性。 例如,Partial<T>
定义为:
type Partial<T> = {
[P in keyof T]?: T[P];
};
使用条件类型和映射类型的最佳实践
虽然条件类型和映射类型很强大,但如果使用不当,它们也会使您的代码更加复杂。 以下是一些需要牢记的最佳实践:
- 保持简单:避免过于复杂的条件类型和映射类型。 如果类型定义变得过于复杂,请考虑将其分解为更小、更易于管理的部分。
- 使用有意义的名称:为您的条件类型和映射类型提供描述性名称,清楚地表明其目的。
- 记录您的类型:添加注释以解释您的条件类型和映射类型背后的逻辑,尤其是在它们很复杂的情况下。
- 利用工具类型:在创建自定义条件或映射类型之前,请检查内置工具类型是否可以实现相同的结果。
- 测试您的类型:通过编写涵盖不同场景的单元测试,确保您的条件类型和映射类型的行为符合预期。
- 考虑性能:复杂的类型计算可能会影响编译时间。 注意您的类型定义的性能影响。
结论
条件类型和映射类型是掌握 TypeScript 的基本工具。 它们使您能够创建高度灵活、类型安全且可维护的应用程序,这些应用程序可以适应复杂的数据结构和动态需求。 通过理解和应用本指南中讨论的概念,您可以释放 TypeScript 的全部潜力,并编写更强大、更具可扩展性的代码。 在您继续探索 TypeScript 时,请记住尝试不同的条件类型和映射类型组合,以发现解决具有挑战性的类型问题的新方法。 可能性确实是无穷无尽的。