泛型
泛型(Generics)是一种编程语言特性,允许在定义函数、类、接口等时使用占位符来表示类型,而不是具体的类型。泛型是一种在编写可重用、灵活且类型安全的代码时非常有用的功能。使用泛型的主要目的是为了处理不特定类型的数据,使得代码可以适用于多种数据类型而不失去类型检查。
- 代码重用: 可以编写与特定类型无关的通用代码,提高代码的复用性。
- 类型安全: 在编译时进行类型检查,避免在运行时出现类型错误。
- 抽象性: 允许编写更抽象和通用的代码,适应不同的数据类型和数据结构。
泛型标识符
在泛型中,通常使用一些约定俗成的标识符,比如常见的 T(表示 Type)、U、V 等,但实际上你可以使用任何标识符。关键是使得代码易读和易于理解,所以建议在泛型类型参数上使用描述性的名称,以便于理解其用途。
标识符 T
代表 "Type",是最常见的泛型类型参数名。
function identity<T>(arg: T): T {
return arg;
}
标识符 K, V
用于表示键(Key)和值(Value)的泛型类型参数。
interface KeyValuePair<K, V> {
key: K;
value: V;
}
标识符 E
用于表示数组元素的泛型类型参数。
function printArray<E>(arr: E[]): void {
arr.forEach((item) => console.log(item));
}
标识符 R
用于表示函数返回值的泛型类型参数。
function getResult<R>(value: R): R {
return value;
}
标识符 U, V
通常用于表示第二、第三个泛型类型参数。
function combine<U, V>(first: U, second: V): string {
return `${first} ${second}`;
}
泛型函数
使用泛型来创建一个 lodash 中的 map 的函数。
function map<E, R>(arr: E[], fn: (arg: E) => R): R[] {
return arr.map(fn);
}
map<number, string>([1, 3, 4], (number) => `${number}`);
以上例子中,map 是一个泛型函数,它有两个类型参数 E 和 R。含义是,原始数组的类型为 E[],对该数组的每个成员执行一个处理函数 fn,将类型 E 转成类型 R,那么就会得到一个类型为 R[] 的数组。
泛型接口
可以使用泛型来定义接口,使接口的成员能够使用任意类型。
interface A<T, U, V> {
point: T;
line: U;
surface: V;
}
interface Point {
x: number;
y: number;
}
interface Line {
length: string;
}
let l: A<Point, Line, number> = {
point: { x: 100, y: 100 },
line: { length: "1000" },
surface: 100000,
};
这里定义了一个泛型接口 A,它有三个类型参数 T 、U 和 V。然后,使用这个泛型接口创建了一个对象 l,其中 point 是 Point 类型,line 是 Line 类型,surface 是 number 类型。
泛型类
泛型也可以应用于类的实例变量和方法:
class Pair<T, U> {
private key: T;
private value: U;
constructor(key: T, value: U) {
this.key = key;
this.value = value;
}
getValue(): U {
return this.value;
}
getKey(): T {
return this.key;
}
getObject(): { key: T; value: U } {
return {
key: this.key,
value: this.value,
};
}
}
const pair = new Pair<string, boolean>("flag", true);
class A extends Pair<string, unknown> {}
class B extends Pair<unknown, unknown> {}
const obj = new A(1, 2); // error 类型“number”的参数不能赋给类型“string”的参数。
const obj1 = new A("name", "rinvay");
const o = new B(1, 2);
const o1 = new B("name", "rinvay");
在这个例子中,Pair 是一个泛型类,使用 T, U 表示泛型类型,构造函数和方法都可以使用泛型类型 T, U。
创建 Pair 实例 pair 时,需要同时给出类型参数 T, U 和类参数 key, value 的值。
class A 和 class B分别继承了 class Pair 在继承泛型类时也需要给出 T, U 的值。通过实例化 A, B,我们分别创建了存储对象的实例 obj、obj1 和 o、o1。创建实例的时候参数需要和继承声明的类型保持一致。
泛型约束
如果想限制泛型的类型范围,可以使用泛型约束,以限制可以在运行时使用的具体类型。
interface LengthSet {
length: number;
}
function logMaxLength<T extends LengthSet>(arr: T[], maxLength: number): void {
for (let i = 0; i < arr.length; i++) {
if (arr[i].length > maxLength) {
console.log(`${arr[i]} is greater than ${maxLength}`);
}
}
}
const names: string[] = ["Alice", "Bob", "Charlie", "David"];
/**
* Outputs
* Alice is greater than 3
* Charlie is greater than 3
* David is greater than 3
*/
logMaxLength(names, 3);
在这个例子中,我们定义了一个 LengthSet 接口,该接口包含一个 length 属性。然后,我们创建了一个 logMaxLength 函数,该函数接受一个类型为 T 的数组和一个 maxLength 参数。通过使用泛型约束,我们限制了 T 必须是一个 LengthSet 类型,这意味着数组中的每个元素都必须有一个 length 属性。这样,我们就可以在函数中安全地访问和比较这些属性的值。
泛型与默认值
范型参数可以设置默认值,在使用过程中,如果没有给出类型参数的值,就会使用该默认值。
function defaultValue<T = string>(arg: T): T {
return arg;
}
let result2 = defaultValue(42);
let res = defaultValue("rinvay");
T = string 表示类型参数的默认值是 string。调用 defaultValue() 时,如果咩有给 T 的值,会使用默认类型 string。