TypeScript 的泛型(Generics)是允许我们在定义函数、接口或类的时候,不预先指定具体类型,而是在使用时再指定类型的机制。这为代码提供了更大的灵活性和复用性,同时保持了类型安全。
泛型标识符
在 TypeScript 中,泛型标识符(也称为类型参数)是用来表示泛型函数、类或接口中未知类型的占位符。通常我们会使用单个大写字母作为标识符的名字,如 T
、U
、V
等等,但也可以使用更具描述性的名称来提高代码的可读性。例如,如果一个泛型参数代表键值对中的键,可以将其命名为 K
;如果它代表一个元素,可以命名为 E
或 Element
。
下面是一些常见的泛型标识符命名习惯:
- T - Type 的缩写,通常用于第一个泛型参数。
- U, V - 用于后续的泛型参数。
- K - Key 的缩写,当泛型参数代表键时使用。
- V - Value 的缩写,当泛型参数代表值时使用。
- E - Element 的缩写,当泛型参数代表集合中的元素时使用。
- TResult - Result Type 的缩写,当泛型参数代表操作的结果类型时使用。
- TItem - Item Type 的缩写,当泛型参数代表某个项目或条目的类型时使用。
示例:使用具描述性的泛型标识符
typescript">// 定义一个泛型函数 identity,使用更具描述性的泛型标识符 TElement 表示元素类型
function identity<TElement>(arg: TElement): TElement {return arg;
}// 使用字符串类型调用泛型函数
let output = identity<string>("myString");
console.log(output); // 输出: myString// 使用数字类型调用泛型函数
output = identity<number>(42);
console.log(output); // 输出: 42
泛型约束与标识符
当你需要确保传入的类型具有某些特定属性或方法时,你可以使用泛型约束。在这种情况下,你可能会选择一个能反映约束条件的标识符名称。
typescript">// 定义一个接口,用于约束泛型类型必须包含 length 属性
interface Lengthwise {length: number;
}// 创建一个泛型类 GenericClass,它接受一个类型参数 T,且 T 必须满足 Lengthwise 接口的要求
class GenericClass<T extends Lengthwise> {constructor(public item: T) {}// 定义一个 showLength 方法,它返回 item 的长度showLength(): number {return this.item.length;}
}// 使用数组类型实例化泛型类,因为数组有 length 属性
let arrayInstance = new GenericClass([1, 2, 3]);
console.log(arrayInstance.showLength()); // 输出: 3// 使用字符串类型实例化泛型类,因为字符串也有 length 属性
let stringInstance = new GenericClass("hello");
console.log(stringInstance.showLength()); // 输出: 5
在这个例子中,T
是一个泛型标识符,但是通过 extends Lengthwise
我们为 T
添加了约束,确保任何被传递给 GenericClass
的类型都必须至少有一个 length
属性。这不仅使得代码更加安全,而且提高了可读性和意图表达。
选择适当的泛型标识符可以帮助其他开发者更容易理解你的代码,特别是在处理复杂的泛型结构时。
泛型函数
当然,下面我将给出两个使用 TypeScript 泛型函数的示例。这些例子将展示如何创建可以处理多种类型的函数,同时保持类型安全。
示例 1: 简单的身份函数
这个例子展示了一个最简单的泛型函数——身份函数(identity function),它接收一个参数并返回相同的值。我们将使用泛型来确保输入和输出的类型一致。
typescript">// 定义一个泛型函数 identity,它可以接受任何类型的参数 T 并返回相同类型的值。
function identity<T>(arg: T): T {return arg;
}// 使用字符串类型调用泛型函数
let outputString = identity<string>("myString");
console.log(outputString); // 输出: myString// 使用数字类型调用泛型函数
let outputNumber = identity<number>(42);
console.log(outputNumber); // 输出: 42// TypeScript 的类型推断机制也可以自动识别类型,无需显式指定
let inferredOutput = identity("Hello, world!");
console.log(inferredOutput); // 输出: Hello, world!
在这个例子中,identity
函数可以处理任意类型的输入,并且在编译时确保输入和输出的类型匹配。TypeScript 的类型推断机制允许我们省略显式的类型参数,因为编译器可以根据传递给函数的实际参数推断出类型。
示例 2: 带有泛型约束的函数
接下来的例子展示了如何定义一个带有泛型约束的函数。这意味着我们可以限制泛型可以接受的类型范围,以确保某些属性或方法的存在。我们将创建一个函数 printLength
,它只接受具有 length
属性的类型作为参数。
typescript">// 定义一个接口,用于约束泛型类型必须包含 length 属性
interface Lengthwise {length: number;
}// 定义一个泛型函数 printLength,它接收一个符合 Lengthwise 接口的对象作为参数
function printLength<T extends Lengthwise>(arg: T): void {console.log(`The length is ${arg.length}`);
}// 使用数组类型调用泛型函数,因为数组有 length 属性
printLength([1, 2, 3]); // 输出: The length is 3// 使用字符串类型调用泛型函数,因为字符串也有 length 属性
printLength("hello"); // 输出: The length is 5// 尝试使用不满足约束条件的类型会引发编译错误
// printLength(42); // 错误:数字没有 length 属性
在这个例子中,printLength
函数使用了泛型约束 <T extends Lengthwise>
,这保证了传入的参数必须有一个 length
属性。如果尝试传递一个不符合约束条件的类型(例如一个数字),TypeScript 编译器将在编译时抛出错误,从而确保了代码的安全性。
这两个示例展示了如何在 TypeScript 中有效地使用泛型函数,既提高了代码的灵活性和复用性,又保持了类型安全性。
泛型接口
下面我将给出两个使用 TypeScript 泛型接口的示例。这些例子展示了如何创建可以处理多种类型的接口,并确保实现该接口的对象或函数能够保持类型安全。
示例 1: 泛型接口与对象
在这个例子中,我们将定义一个泛型接口 Box
,它可以用于包装任何类型的值。我们还将展示如何创建符合此接口的对象实例。
typescript">// 定义一个泛型接口 Box,它接受一个类型参数 T。
// 这个接口表示一个可以存储任意类型值的容器。
interface Box<T> {content: T;
}// 创建一个符合 Box<string> 接口的对象实例,表示一个包含字符串内容的盒子。
let stringBox: Box<string> = {content: "Hello, world!"
};// 创建一个符合 Box<number> 接口的对象实例,表示一个包含数字内容的盒子。
let numberBox: Box<number> = {content: 42
};console.log(stringBox.content); // 输出: Hello, world!
console.log(numberBox.content); // 输出: 42
在这个例子中,Box
接口是一个泛型接口,它允许我们在创建对象时指定所要存储的数据类型。这使得我们可以创建多个不同类型的 Box
实例,同时保证了类型的安全性。
示例 2: 泛型接口与函数
接下来的例子展示了如何定义一个泛型接口来描述函数的签名,并且这个函数可以接受和返回任意类型的参数。我们将创建一个函数,该函数接收一个数组并返回该数组的第一个元素(如果存在)。
typescript">// 定义一个泛型接口 IdentityFn,它接受一个类型参数 T。
// 这个接口描述了一个函数,该函数接收一个 T 类型的数组并返回 T 类型的单个值。
interface IdentityFn<T> {(values: T[]): T | undefined;
}// 创建一个符合 IdentityFn 接口的函数 firstElement,它返回数组的第一个元素。
function firstElement<T>(values: T[]): T | undefined {return values.length > 0 ? values[0] : undefined;
}// 检查 firstElement 函数是否符合 IdentityFn 接口
let myIdentityFn: IdentityFn<number> = firstElement;console.log(myIdentityFn([1, 2, 3])); // 输出: 1
console.log(myIdentityFn([])); // 输出: undefined// 使用不同的类型调用函数
console.log(firstElement(["apple", "banana", "cherry"])); // 输出: apple
在这个例子中,IdentityFn
接口是一个泛型接口,它描述了一个函数的签名,该函数接收一个类型为 T[]
的数组,并返回类型为 T
或 undefined
的单个值。通过这种方式,我们可以确保 firstElement
函数在编译时是类型安全的,并且可以根据传入的数组类型正确地推断返回值的类型。
这两个示例展示了如何在 TypeScript 中使用泛型接口来提高代码的灵活性和复用性,同时也确保了类型的安全性和一致性。
泛型类
当然,下面我将给出两个使用 TypeScript 泛型类的示例。这些例子展示了如何创建可以处理多种类型的类,并确保实例化该类的对象能够保持类型安全。
示例 1: 泛型类作为容器
在这个例子中,我们将定义一个泛型类 Container
,它可以用于存储任何类型的值。我们还将展示如何创建符合此类的实例,并操作其内容。
typescript">// 定义一个泛型类 Container,它接受一个类型参数 T。
// 这个类表示一个可以存储任意类型值的容器。
class Container<T> {private content: T;constructor(content: T) {this.content = content;}// 获取容器中的内容getContent(): T {return this.content;}// 设置容器中的内容setContent(newContent: T): void {this.content = newContent;}
}// 创建一个包含字符串的 Container 实例
let stringContainer = new Container<string>("Hello, world!");
console.log(stringContainer.getContent()); // 输出: Hello, world!// 创建一个包含数字的 Container 实例
let numberContainer = new Container<number>(42);
console.log(numberContainer.getContent()); // 输出: 42// 修改容器的内容
stringContainer.setContent("Goodbye, world!");
console.log(stringContainer.getContent()); // 输出: Goodbye, world!
在这个例子中,Container
类是一个泛型类,它允许我们在创建对象时指定所要存储的数据类型。这使得我们可以创建多个不同类型的 Container
实例,同时保证了类型的安全性。
示例 2: 泛型类与方法约束
接下来的例子展示了如何定义一个带有泛型参数和方法约束的类。我们将创建一个 Stack
类,它可以用来实现栈数据结构,并且只能存储具有特定属性或方法的类型。
typescript">// 定义一个接口,用于约束泛型类型必须包含 length 属性
interface Lengthwise {length: number;
}// 定义一个泛型类 Stack,它接受一个类型参数 T,且 T 必须满足 Lengthwise 接口的要求。
class Stack<T extends Lengthwise> {private items: T[] = [];// 向栈中添加元素push(item: T): void {this.items.push(item);}// 从栈中弹出元素并返回pop(): T | undefined {return this.items.pop();}// 打印栈顶元素的长度printTopLength(): void {const topItem = this.items[this.items.length - 1];if (topItem) {console.log(`The length of the top item is ${topItem.length}`);} else {console.log('The stack is empty.');}}
}// 使用数组类型实例化泛型类,因为数组有 length 属性
let arrayStack = new Stack<[number]>();
arrayStack.push([1, 2, 3]);
arrayStack.printTopLength(); // 输出: The length of the top item is 3// 使用字符串类型实例化泛型类,因为字符串也有 length 属性
let stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.printTopLength(); // 输出: The length of the top item is 5// 尝试使用不满足约束条件的类型会引发编译错误
// let numberStack = new Stack<number>(); // 错误:数字没有 length 属性
在这个例子中,Stack
类是一个泛型类,并且它的泛型参数 T
被约束为必须实现 Lengthwise
接口。这意味着传入的类型必须至少有一个 length
属性。通过这种方式,我们可以在类的方法中安全地访问和操作这个属性,同时仍然允许不同的具体类型被传递给泛型参数。
这两个示例展示了如何在 TypeScript 中使用泛型类来提高代码的灵活性和复用性,同时也确保了类型的安全性和一致性。泛型类不仅可以让代码更通用,还能帮助开发者编写更加健壮的应用程序。
泛型与默认值
在 TypeScript 中,泛型可以与默认类型参数一起使用。这允许我们在定义泛型时提供一个或多个类型的默认值。如果用户没有明确指定类型参数,TypeScript 将使用这些默认值。从 TypeScript 2.3 开始,支持为泛型类型参数设置默认类型。
示例 1: 泛型函数与默认类型
在这个例子中,我们将创建一个泛型函数 createTuple
,它接收两个参数并返回一个元组(tuple)。我们将为第二个参数的类型提供一个默认类型 string
。
typescript">// 定义一个泛型函数 createTuple,它可以接受两个不同类型的参数,并返回一个元组。
function createTuple<T, U = string>(first: T, second: U): [T, U] {return [first, second];
}// 使用数字和字符串调用泛型函数
let tuple1 = createTuple<number, string>(10, "hello");
console.log(tuple1); // 输出: [10, "hello"]// 只指定第一个参数的类型,第二个参数将使用默认类型 string
let tuple2 = createTuple<number>(20, "world");
console.log(tuple2); // 输出: [20, "world"]// 如果不指定类型参数,TypeScript 的类型推断机制会自动识别类型
let tuple3 = createTuple("TypeScript", "is awesome");
console.log(tuple3); // 输出: ["TypeScript", "is awesome"]
在这个例子中,createTuple
函数的第二个类型参数 U
被赋予了默认类型 string
。因此,如果我们只指定了第一个参数的类型,或者完全依赖于类型推断,那么第二个参数将会默认为字符串类型。
示例 2: 泛型类与默认类型
接下来的例子展示了如何为泛型类提供默认类型参数。我们将创建一个简单的 Box
类,它可以存储任何类型的值,并且我们将为这个类型参数提供一个默认类型 number
。
typescript">// 定义一个泛型类 Box,它接受一个类型参数 T,默认为 number。
class Box<T = number> {private content: T;constructor(content: T) {this.content = content;}// 获取容器中的内容getContent(): T {return this.content;}// 设置容器中的内容setContent(newContent: T): void {this.content = newContent;}
}// 创建一个包含数字的 Box 实例,使用默认类型 number
let numberBox = new Box(42);
console.log(numberBox.getContent()); // 输出: 42// 创建一个包含字符串的 Box 实例,显式指定类型为 string
let stringBox = new Box<string>("Hello, world!");
console.log(stringBox.getContent()); // 输出: Hello, world!// 创建一个包含布尔值的 Box 实例,显式指定类型为 boolean
let booleanBox = new Box<boolean>(true);
console.log(booleanBox.getContent()); // 输出: true
在这个例子中,Box
类的类型参数 T
被赋予了默认类型 number
。因此,如果我们不指定类型参数,Box
类将默认处理 number
类型的数据。然而,我们仍然可以通过显式地传递类型参数来覆盖默认类型,如上面的 stringBox
和 booleanBox
所示。
这两个示例展示了如何在 TypeScript 中使用泛型的默认类型参数。这种方式不仅提高了代码的灵活性,还使得开发者在不需要特别关注所有类型参数的情况下更方便地使用泛型。