TypeScript泛型编程的六大实战模式
模式一:函数泛型
在TypeScript中,函数泛型是一种强大的特性,它允许我们在定义函数时使用类型参数,从而使函数能够处理不同类型的数据,同时保持类型安全。
基础函数泛型示例
假设我们要创建一个函数,该函数可以接受任意类型的参数并返回该参数。使用函数泛型可以这样实现:
function identity<T>(arg: T): T {
return arg;
}
let result = identity<string>("Hello, TypeScript!");
console.log(result);
在上述代码中,<T>
是类型参数,它代表一个未知的类型。arg: T
表示参数 arg
的类型是 T
,而函数的返回值类型也是 T
。通过在调用 identity
函数时指定 <string>
,我们告诉TypeScript这里的 T
就是 string
类型。
多个类型参数
函数泛型并不局限于单个类型参数,我们可以定义多个类型参数。例如,创建一个函数,它接受两个不同类型的参数并返回一个包含这两个参数的数组:
function pair<U, V>(first: U, second: V): [U, V] {
return [first, second];
}
let pairResult = pair<number, string>(42, "answer");
console.log(pairResult);
这里定义了两个类型参数 U
和 V
,分别用于表示 first
和 second
参数的类型,返回值类型是一个包含 U
和 V
类型元素的元组。
类型参数的约束
有时候,我们希望类型参数满足一定的条件。例如,我们想要创建一个函数,它接受一个数组和一个索引,返回数组中指定索引位置的元素。但我们希望这个数组类型具有 length
属性,以确保可以安全地访问索引。这时可以使用类型参数约束:
function getElement<T extends { length: number }>(arr: T, index: number): T extends any[]? T[number] : never {
if (index >= 0 && index < arr.length) {
return arr[index];
}
return undefined as never;
}
let numbers = [1, 2, 3];
let element = getElement(numbers, 1);
console.log(element);
这里 <T extends { length: number }>
表示类型参数 T
必须是一个具有 length
属性的类型。T extends any[]? T[number] : never
用于在 T
是数组类型时返回数组元素类型,否则返回 never
类型。
模式二:泛型接口
泛型接口允许我们在接口定义中使用类型参数,从而创建可复用的接口结构,适用于不同类型的数据。
简单泛型接口示例
定义一个泛型接口 Box
,它有一个 value
属性,类型由类型参数决定:
interface Box<T> {
value: T;
}
let numberBox: Box<number> = { value: 42 };
let stringBox: Box<string> = { value: "Hello" };
在上述代码中,Box<T>
是一个泛型接口,T
是类型参数。通过 Box<number>
和 Box<string>
,我们创建了针对 number
和 string
类型的具体接口实例。
泛型接口与函数类型
泛型接口也可以用于定义函数类型。例如,定义一个泛型接口 Mapper
,它描述了一个接受一个参数并返回另一个类型值的函数:
interface Mapper<T, U> {
(arg: T): U;
}
let stringToNumberMapper: Mapper<string, number> = (str) => parseInt(str);
let resultMapper = stringToNumberMapper("42");
console.log(resultMapper);
这里 Mapper<T, U>
定义了一个函数类型,T
是参数类型,U
是返回值类型。stringToNumberMapper
是一个符合该接口的具体函数。
继承泛型接口
我们可以让一个接口继承另一个泛型接口,并进一步约束类型参数。例如:
interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): void;
}
interface Container<T extends Animal> {
item: T;
}
interface DogContainer extends Container<Dog> {
playWithDog(): void;
}
let myDog: Dog = { name: "Buddy", bark() { console.log("Woof!"); } };
let dogContainer: DogContainer = { item: myDog, playWithDog() { console.log("Playing with Buddy!"); } };
这里 Container<T>
是一个泛型接口,T
约束为 Animal
类型或其子类型。DogContainer
继承自 Container<Dog>
,并添加了 playWithDog
方法。
模式三:泛型类
泛型类允许我们在类的定义中使用类型参数,使类能够处理不同类型的数据,同时保持类型安全。
基础泛型类示例
定义一个简单的泛型类 Stack
,用于实现一个栈数据结构:
class Stack<T> {
private items: T[] = [];
push(item: T) {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
let numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
let poppedNumber = numberStack.pop();
console.log(poppedNumber);
在这个 Stack<T>
类中,T
是类型参数,items
数组的类型是 T[]
,push
方法接受类型为 T
的参数,pop
方法返回类型为 T | undefined
。
泛型类的继承
泛型类也可以被继承,并且子类可以继续使用或重新定义类型参数。例如:
class Queue<T> {
private items: T[] = [];
enqueue(item: T) {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
}
class PriorityQueue<T extends { priority: number }> extends Queue<T> {
enqueue(item: T) {
let inserted = false;
for (let i = 0; i < this.items.length; i++) {
if (item.priority < this.items[i].priority) {
this.items.splice(i, 0, item);
inserted = true;
break;
}
}
if (!inserted) {
this.items.push(item);
}
}
}
let task1 = { priority: 2, description: "Second task" };
let task2 = { priority: 1, description: "First task" };
let priorityQueue = new PriorityQueue();
priorityQueue.enqueue(task1);
priorityQueue.enqueue(task2);
let dequeuedTask = priorityQueue.dequeue();
console.log(dequeuedTask);
这里 Queue<T>
是一个泛型类,PriorityQueue<T extends { priority: number }>
继承自 Queue<T>
,并对 T
进行了约束,要求 T
类型具有 priority
属性。PriorityQueue
重写了 enqueue
方法以实现按优先级入队。
静态成员与泛型
需要注意的是,泛型类的静态成员不能使用类的类型参数。因为静态成员属于类本身,而不是类的实例,在类被实例化之前,类型参数的值是不确定的。例如:
class StaticGenericClass<T> {
static someStaticMethod(): T {
// 这会报错,因为静态方法不能使用类的类型参数T
return undefined as any;
}
}
如果需要在静态方法中使用泛型,可以单独为静态方法定义类型参数:
class StaticGenericClass {
static someStaticMethod<U>(arg: U): U {
return arg;
}
}
let staticResult = StaticGenericClass.someStaticMethod<string>("Static result");
console.log(staticResult);
模式四:泛型约束与条件类型
泛型约束和条件类型是TypeScript中非常强大的功能,它们可以让我们根据类型参数的不同,进行不同的类型推导和处理。
泛型约束回顾
在前面的例子中,我们已经使用过泛型约束。例如,T extends { length: number }
表示类型参数 T
必须具有 length
属性。泛型约束可以确保在泛型代码中能够安全地访问特定的属性或方法。
条件类型基础
条件类型使用 T extends U? X : Y
的语法,它的含义是:如果类型 T
可以赋值给类型 U
,则结果为类型 X
,否则为类型 Y
。例如:
type IsString<T> = T extends string? true : false;
type StringCheck = IsString<string>;
type NumberCheck = IsString<number>;
在上述代码中,IsString<T>
是一个条件类型,StringCheck
的值为 true
,因为 string
类型可以赋值给 string
类型;NumberCheck
的值为 false
,因为 number
类型不能赋值给 string
类型。
条件类型与泛型结合
我们可以将条件类型与泛型结合,实现更复杂的类型推导。例如,定义一个类型 Flatten
,它可以将数组类型展开为元素类型,如果不是数组类型则保持不变:
type Flatten<T> = T extends Array<infer U>? U : T;
type StringArrayFlatten = Flatten<string[]>;
type NumberFlatten = Flatten<number>;
这里 infer U
用于推断出数组元素的类型。如果 T
是数组类型,Flatten<T>
的结果就是数组元素的类型;否则就是 T
本身。
分布式条件类型
当条件类型作用于泛型类型参数,并且该参数是裸类型参数(没有被其他类型包装)时,会产生分布式条件类型。例如:
type ToArray<T> = T extends any? T[] : never;
type StringToArray = ToArray<string>;
type UnionToArray = ToArray<string | number>;
对于 ToArray<string | number>
,TypeScript会将其分布式地应用到联合类型的每个成员上,结果为 string[] | number[]
。
模式五:映射类型
映射类型是TypeScript中一种基于现有类型创建新类型的强大方式,它可以对类型的属性进行逐个转换。
基础映射类型示例
假设我们有一个类型 User
:
interface User {
name: string;
age: number;
email: string;
}
type ReadonlyUser = {
readonly [P in keyof User]: User[P];
};
let readonlyUser: ReadonlyUser = { name: "John", age: 30, email: "john@example.com" };
// readonlyUser.name = "Jane"; // 这会报错,因为属性是只读的
在 ReadonlyUser
类型中,[P in keyof User]
表示遍历 User
类型的所有属性键,readonly User[P]
表示将每个属性变为只读,类型保持不变。
可选属性映射
我们可以将一个类型的所有属性变为可选。例如:
type OptionalUser = {
[P in keyof User]?: User[P];
};
let optionalUser: OptionalUser = { name: "Jane" };
这里通过在属性定义中添加 ?
,将 User
类型的所有属性变为可选。
移除特定属性
可以使用映射类型移除某个类型的特定属性。例如,移除 User
类型中的 email
属性:
type UserWithoutEmail = {
[P in Exclude<keyof User, "email">]: User[P];
};
let userWithoutEmail: UserWithoutEmail = { name: "Bob", age: 25 };
Exclude<keyof User, "email">
用于排除 email
属性键,然后通过映射类型创建一个新类型,只包含剩余的属性。
模式六:高级类型组合
在实际开发中,我们经常需要将多种泛型编程模式组合使用,以实现复杂的类型需求。
结合泛型类、接口与条件类型
例如,我们要创建一个 DataProcessor
类,它可以根据数据的类型进行不同的处理。如果数据是数组,我们对数组元素进行某种操作;如果不是数组,则直接返回数据。
interface Processor<T> {
process(data: T): T;
}
class DataProcessor<T> {
private processor: Processor<T>;
constructor(processor: Processor<T>) {
this.processor = processor;
}
handleData(data: T): T {
return this.processor.process(data);
}
}
type ArrayProcessor<T> = T extends Array<infer U>? (value: U) => U : never;
class ArrayDataProcessor<T extends Array<any>> implements Processor<T> {
private itemProcessor: ArrayProcessor<T>;
constructor(itemProcessor: ArrayProcessor<T>) {
this.itemProcessor = itemProcessor;
}
process(data: T): T {
return data.map((item) => this.itemProcessor(item)) as T;
}
}
class SingleDataProcessor<T> implements Processor<T> {
process(data: T): T {
return data;
}
}
let numberArrayProcessor = new ArrayDataProcessor<number[]>(value => value * 2);
let numberDataProcessor = new DataProcessor<number[]>(numberArrayProcessor);
let resultArray = numberDataProcessor.handleData([1, 2, 3]);
console.log(resultArray);
let singleDataProcessor = new DataProcessor<string>(new SingleDataProcessor<string>());
let resultSingle = singleDataProcessor.handleData("Hello");
console.log(resultSingle);
在这段代码中,Processor
接口定义了处理数据的方法。DataProcessor
类接受一个 Processor
实例并调用其 process
方法。ArrayProcessor
是一个条件类型,用于处理数组元素的处理器类型。ArrayDataProcessor
实现了 Processor
接口,专门处理数组数据。SingleDataProcessor
则处理非数组数据。
映射类型与条件类型的结合
再比如,我们有一个类型 Product
,它有一些属性,我们希望创建一个新类型,对于字符串类型的属性变为只读,其他类型属性保持不变。
interface Product {
name: string;
price: number;
description: string;
}
type ModifiedProduct = {
[P in keyof Product]: Product[P] extends string? readonly Product[P] : Product[P];
};
let modifiedProduct: ModifiedProduct = { name: "Widget", price: 10, description: "A useful widget" };
// modifiedProduct.name = "New Widget"; // 这会报错,因为name属性是只读的
这里通过结合映射类型和条件类型,遍历 Product
类型的所有属性,对于字符串类型的属性变为只读,其他类型属性保持不变。
通过这些实战模式的学习和应用,我们能够在TypeScript中充分发挥泛型编程的优势,编写出更加灵活、可复用且类型安全的代码,提升开发效率和代码质量。无论是小型项目还是大型企业级应用,这些模式都能为我们的开发工作带来极大的便利。