9. TypeScript 泛型
TypeScript 中的泛型使开发者能够编写灵活、可重用的代码,同时保持类型安全。它们允许动态定义类型,确保函数、类和接口可以适用于任何数据类型。这有助于避免重复,提高代码的模块化,使其既类型安全又具备适应性。
一、认识泛型
当程序员需要创建可重用组件时,可以使用 TypeScript 的泛型类型,因为它们用于创建可以处理各种数据类型的组件,并提供类型安全性。可重用组件可以是类、函数和接口。TypeScript 泛型可以以多种方式使用,例如函数泛型、类泛型和接口泛型。
(一) 语法
function functionName<T> (returnValue : T) : T {return returnValue;
}
(二) 用法示例
1. 例一
在这个例子中,我们创建了一个可以接受任意类型数据的泛型函数。我们只需要在参数中传入任何类型的数据,然后就可以通过 reverseArray
函数对该值进行反转。
function reverseArray<T>(array: T[]): T[] {return array.reverse();
}const strArray: string[] = reverseArray(["Java", "Python", "C++"]);
const numArray: number[] = reverseArray([1, 2, 3, 4, 5]);
const boolArray: boolean[] = reverseArray([false, true]);console.log(strArray);
console.log(numArray);
console.log(boolArray);
输出:
[ 'C++', 'Python', 'Java' ]
[ 5, 4, 3, 2, 1 ]
[ true, false ]
2. 例二
在这个例子中,我们创建了一个泛型接口,通过它我们创建了一个对象和字符串类型的值,并将它们打印在控制台中。
// 定义一个泛型接口 Resource,T 是类型参数
interface Resource<T> {id: number; // 资源 IDresourceName: string; // 资源名称data: T; // 泛型数据字段,类型由使用者决定
}// 使用对象类型实现泛型接口
const person: Resource<object> = {id: 1, // 资源 ID 为 1resourceName: 'Person', // 资源名称为 "Person"data: { // data 是一个对象类型name: 'John', // 姓名age: 25 // 年龄}
}
console.log(person); // 打印 person 对象// 使用字符串数组类型实现泛型接口
const employee: Resource<string[]> = {id: 2, // 资源 ID 为 2resourceName: 'Employee', // 资源名称为 "Employee"data: ['Employee 1', 'Employee 1'] // data 是一个字符串数组
}
console.log(employee); // 打印 employee 对象
输出:
{"id": 1,"resourceName": "Person","data": {"name": "John","age": 25}
}
{"id": 2,"resourceName": "Employee","data": ["Employee 1","Employee 1"]
}
二、泛型函数
TypeScript 泛型函数允许你创建可以处理多种类型的数据的函数,同时保持类型安全。通过使用尖括号(<T>
)中定义的类型参数,泛型使函数能够在不同的数据类型上运行,而不会失去 TypeScript 类型检查所带来的优势。
(一) 语法
function functionName<T>(parameterName: T): ReturnType {// 函数实现
}
这里:
functionName
是你的泛型函数的名称。<T>
是类型参数T
,它允许你在函数内部处理不同的数据类型。parameterName
表示函数参数的名称,T
指定该参数接受的数据类型。ReturnType
指定函数预期返回值的类型。
(二) 用法示例
1. 例一
TypeScript 中带有单个类型参数的泛型函数能够处理多种数据类型,同时保证类型安全。你可以显式指定类型,也可以让 TypeScript 推断参数和返回值的类型。
function foo<T>(arg: T): T {return arg;
}let result1: string = foo<string>("TYPESCRIPT");
let result2: number = foo<number>(740);
let result3: boolean = foo<boolean>(false);console.log(result1);
console.log(result2);
console.log(result3);
输出:
GEEKSFORGEEKS
740
false
2. 例二
带有数组参数的泛型函数允许你处理不同类型的数组。通过使用类型参数,该函数可以处理各种元素类型的数组,同时保持类型安全。
function arrayEl<T>(arr: T[]): void {for (const x of arr) {console.log(x);}
}let elements: number[] = [101, 102, 103];
arrayEl(elements);let elements1: string[] = ["吃饭", "睡觉", "打豆豆"];
arrayEl(elements1);
输出:
101
102
103
吃饭
睡觉
打豆豆
3. 例三
在这个例子中,函数 mergeArrays 将两种不同类型的数组合并为一个。它接收类型为 T 和 U 的数组,返回类型为 (T | U)[] 的数组。
function mergeArrays<T, U>(arr1: T[], arr2: U[]): (T | U)[] {return [...arr1, ...arr2];
}// 不同类型的数组
const numbers: number[] = [1, 2, 3];
const words: string[] = ["hello", "world"];// 合并不同类型的数组
const mergedArray: (number | string)[] = mergeArrays(numbers, words);// 输出合并后的数组
console.log(mergedArray);
输出:
[1, 2, 3, "hello", "world"]
(三) 总结
在 TypeScript 中,泛型函数允许你构建能够处理不同数据类型的通用函数,同时确保代码的类型安全,避免错误。这种灵活性非常实用,尤其是在你希望创建能够应对多种情况而无需重复代码的函数时。无论是处理数组、对象还是其他数据结构,泛型函数都能帮助你编写更简洁、更安全的代码。
三、泛型约束
在 TypeScript 中,泛型约束(Generic Constraints)通过使用 extends
关键字来限制可以与泛型类型一起使用的类型。这确保了泛型类型遵循特定的结构或接口,从而能够在泛型中访问某些属性或方法。
(一) 什么是泛型约束?
- TypeScript 的泛型使我们能够编写可重用的代码,能够处理不同的数据类型。我们可以创建可以与不同数据类型一起工作的函数、类或接口。
- 泛型使用
<T>
的形式定义,这种 T 类型可以用于函数参数、返回值等的类型定义。 - 泛型约束用于对泛型类型参数可使用的类型施加限制。
- 这种限制带来了类型检查,确保变量在被使用之前具备某种类型的值。
- 这种检查机制有助于最大限度地减少运行时错误的发生。
语法:
function genericConstraintFunction<T extends GenericConstraintType>(param: T): void {// ...
}
含义如下:
- T 是泛型类型参数。
extends
GenericConstraintType 指定了一个约束条件,要求类型参数 T 必须继承或符合 GenericConstraintType 类型的结构。
(二) 用法示例
1. 例一
在此示例中,Sports
接口具有一个 name
属性。printSportName
函数使用 extends
来确保其参数在执行前符合 Sports
类型的结构。
// 使用 name 属性定义 Sports 接口
interface Sports {name: string;
}// 定义函数,确保传入的类型 T 继承自 Sports
function printSportName<T extends Sports>(sport: T): void {console.log(sport.name); // 输出运动名称
}// 创建一个 Sports 类型的对象
let sport: Sports = { name: "棒球" };// 使用 sport 对象调用函数
printSportName(sport);
输出:
baseball
2. 例二
在此示例中,我们使用 extends keyof
来确保泛型类型参数 K
是类型 T
的一个键。这种做法强制要求 K
必须是 T
的一个有效属性。
interface Sports {name: string;players: number;
}// 泛型函数,限制 T 必须扩展自 Sports,K 必须是 T 的键
function getNumberOfPlayers<T extends Sports, K extends keyof T>(sport: T, players: K): T[K] {return sport[players];}let sport: Sports = { name: "baseball", players: 9 };// 'players' 被推断为 number 类型
let players: number = getNumberOfPlayers(sport, 'players');
console.log(`球员人数为:${players}`);
输出:
球员人数为:9
3. 例三
在此示例中,我们确保泛型类型参数的类对象实现了特定的接口。
// 定义一个接口 Sports
interface Sports {name: string;players: number;getNumberOfGloves(): number; // 获取手套数量的方法
}// 定义一个实现 Sports 接口的类 Baseball
class Baseball implements Sports {constructor(public name: string, public players: number) { }// 实现接口中的方法getNumberOfGloves(): number {return this.players * 2;}
}// 定义一个泛型函数,约束类型 T 必须实现 Sports 接口
function getNumberOfGloves<T extends Sports>(sport: T): void {console.log(`所需手套数量为:${sport.getNumberOfGloves()}`);
}// 创建 Baseball 类的实例
let baseball = new Baseball("baseball", 9);
getNumberOfGloves(baseball); // 输出:所需手套数量为:18
输出:
所需手套数量为:18
四、泛型接口
在 TypeScript 中创建带有泛型类型的接口是一项强大的功能,它提升了代码的灵活性和可复用性。通过定义带有泛型类型参数的接口,你可以创建适用于多种数据类型的组件,同时保持类型安全。
(一) 带有单一泛型类型参数的接口
interface InterfaceName<T> {// 接口成员
}
定义一个带有单一泛型类型参数的接口,可以创建通用组件,这些组件能够在不同数据类型间灵活操作。此方法提升了代码的灵活性,并通过抽象具体的数据实现,促进代码的重用。
下面的代码创建了一个带有单个泛型类型参数的接口。
interface Box<T> {getItem(): T;setItem(item: T): void;
}class MyBox<T> implements Box<T> {private item: T;getItem(): T {return this.item;}setItem(item: T): void {this.item = item;}
}const stringBox: Box<string> = new MyBox<string>();
stringBox.setItem("Hello, Generics!");
console.log("字符串的值:", stringBox.getItem());
输出:
字符串的值:Hello, Generics!
(二) 多泛型类型参数
TypeScript 也支持带有多个泛型类型参数的接口,使开发者能够设计高度可定制且参数化的组件。
下面的代码创建了一个带有多个泛型类型参数的接口。
interface Pair<K, V> {key: K;value: V;
}const pair: Pair<number, string> = { key: 1, value: "One" };
console.log("Key:", pair.key, ", Value:", pair.value);
输出:
Key: 1, Value: One
五、泛型与类
TypeScript 中在泛型中使用类类型,可以让你通过指定泛型参数为类的构造函数,创建更灵活且可重用的代码。当你想操作类的实例,同时又希望保持类型安全时,这种方式特别有用。你可以定义一个接受类构造函数的泛型类型,然后使用该类型来创建和操作这些类的实例。
(一) 语法
function createInstance<T>(constructor: new (...args: any[]) => T,...args: any[]
): T {// 创建并返回指定类的实例return new constructor(...args);
}
说明:
- T 是一个泛型类型参数,表示要创建的实例的类型。它允许你指定工厂函数的期望返回类型。
- constructor 是类构造函数的引用。
- new (...args: any[]) => T 表示该构造函数可以接受任意数量的参数(
...args: any[]
),并返回类型为T
的实例。 - ...args: any[] 使用剩余参数语法,接受任意数量的额外参数,这些参数会在创建实例时传递给构造函数。
(二) 用法示例
1. 例一
在这个例子中,我们定义了一个名为 createInstance 的泛型工厂函数。它接受一个类的构造函数(constructor)和任意数量的附加参数(...args)。createInstance 函数通过使用传入的参数调用构造函数,创建并返回指定类的实例。
// 定义一个泛型工厂函数
function createInstance<T>(constructor:new (...args: any[]) => T, ...args: any[]): T {return new constructor(...args);
}// 示例类
class Product {constructor(public name: string, // 产品名称public price: number) { } // 产品价格
}class Person {constructor(public name: string, // 人名public age: number) { } // 年龄
}// 使用工厂函数创建实例
const laptop = createInstance(Product, 'Laptop', 999);
console.log(laptop);const user = createInstance(Person, 'TypeScript', 30);
console.log(user);
输出:
Product { name: 'Laptop', price: 999 }
Person { name: 'TypeScript', age: 30 }
2. 例二
在这个例子中,我们定义了一个基类 Animal,包含一个 name 属性和一个 makeSound 方法。Dog 类继承自 Animal,新增了 breed 属性并重写了 makeSound 方法。我们创建了一个泛型函数 printAnimalInfo,该函数接受一个类型为 T 的参数,且 T 必须继承自 Animal 类。这意味着它可以接受 Animal 类或其子类的实例。然后我们创建了 Animal 和 Dog 的实例,并分别调用 printAnimalInfo 函数。
// 定义基类 Animal
class Animal {// 构造函数,初始化公开属性 nameconstructor(public name: string) { }// 返回动物发出的通用声音makeSound(): string {return "Some generic animal sound";}
}// 定义继承自 Animal 的子类 Dog
class Dog extends Animal {// 构造函数,接收 name 和 breed 两个参数constructor(name: string, public breed: string) {super(name); // 调用父类构造函数,初始化 name}// 重写父类的 makeSound 方法,返回狗叫声makeSound(): string {return "Woof! Woof!";}
}// 定义泛型函数,要求类型 T 继承自 Animal
function printAnimalInfo<T extends Animal>(animal: T): void {// 打印动物的名字console.log(`Name: ${animal.name}`);// 打印动物的叫声(调用 makeSound 方法)console.log(`Sound: ${animal.makeSound()}`);}// 创建 Animal 类的实例
const genericAnimal = new Animal("Generic Animal");
// 创建 Dog 类的实例
const myDog = new Dog("Buddy", "Golden Retriever");// 使用泛型函数,分别传入 Animal 和 Dog 实例
printAnimalInfo(genericAnimal);
printAnimalInfo(myDog);
输出:
Name: Generic Animal
Sound: Some generic animal sound
Name: Buddy
Sound: Woof! Woof!
六、泛型对象类型
TypeScript 泛型对象类型允许你为对象创建灵活且可复用的类型定义。这些泛型类型可以处理不同结构的对象,同时保证类型安全,确保代码既健壮又具有适应性。它们特别适合用于创建能够处理多种对象结构的函数或类,同时保持类型的正确性。
(一) 语法
type MyGenericObject<T> = {key: string;value: T;
};
(二) 用法示例
1. 键值对
在此示例中,我们创建了一个泛型对象类型来表示键值对。类型参数 T 表示值的类型,使得可以使用不同的数据类型。
type KeyValuePair<T> = {key: string;value: T;
};const stringPair: KeyValuePair<string> = { key: 'name', value: 'John' };
const numberPair: KeyValuePair<number> = { key: 'age', value: 30 };console.log(stringPair);
console.log(numberPair);
输出:
{ key: 'name', value: 'John' }
{ key: 'age', value: 30 }
2. 封装数据属性
在此示例中,我们定义了一个泛型对象类型,用于封装指定类型 T 的数据属性。这个例子展示了泛型对象类型如何存储和访问不同的数据类型。
type DataContainer<T> = {data: T;
};const numericData: DataContainer<number> = { data: 25 };
const stringData: DataContainer<string> = { data: 'TypeScript' };console.log(numericData.data);
console.log(stringData.data);
输出:
25
TypeScript
(三) 总结
TypeScript 的泛型对象类型提供了一种强大的方式来创建灵活、可复用且类型安全的对象定义。通过利用这些类型,你可以构建更健壮且适应性更强的代码,确保函数和类能够处理不同的对象结构,同时保持严格的类型正确性。这种方法提升了代码的可维护性,降低了出错的风险,成为 TypeScript 开发者工具箱中不可或缺的重要工具。