TypeScript 泛型完全指南:从入门到实战
TypeScript 泛型完全指南:从入门到实战
引言
在 TypeScript 的世界里,泛型(Generics)是最强大但也最容易被误解的特性之一。很多开发者在学习 TypeScript 时,会在泛型这里卡住,觉得它过于抽象和复杂。但实际上,泛型是编写可复用、类型安全代码的关键工具。
本文将带你从泛型的基础概念开始,逐步深入到实际应用场景,帮助你真正掌握这一重要特性。无论你是 TypeScript 初学者还是有一定经验的开发者,相信都能从本文中获得收获。
什么是泛型?
泛型,简单来说,就是"类型的参数化"。就像函数可以接受参数一样,泛型允许我们给类型也加上参数。这样,我们就可以编写能够适用于多种类型的代码,而不需要为每种类型都写一份重复的代码。
为什么需要泛型?
让我们先看一个没有使用泛型的例子:
// 没有泛型的情况
function identityString(arg: string): string {
return arg;
}
function identityNumber(arg: number): number {
return arg;
}
function identityBoolean(arg: boolean): boolean {
return arg;
}
上面的代码中,我们为了处理不同类型的参数,不得不编写三个几乎完全相同的函数。这不仅造成了代码冗余,而且如果我们需要支持更多类型,就要继续添加更多的函数。
使用泛型后,我们可以这样写:
// 使用泛型
function identity<T>(arg: T): T {
return arg;
}
// 使用示例
const result1 = identity<string>('Hello'); // 类型:string
const result2 = identity<number>(42); // 类型:number
const result3 = identity<boolean>(true); // 类型:boolean
通过泛型,我们只用一个函数就实现了之前三个函数的功能,而且类型信息完全保留。
泛型基础语法
泛型函数
泛型函数的基本语法是在函数名后面用尖括号声明类型参数:
function identity<T>(arg: T): T {
return arg;
}
这里的 INLINE_CODE_0 就是一个类型参数,它代表"某种类型"。在使用函数时,我们可以指定具体的类型:
const result = identity<string>('Hello TypeScript');
TypeScript 也支持类型推断,很多时候不需要显式指定类型:
const result = identity('Hello TypeScript'); // T 被推断为 string
多个类型参数
泛型函数可以有多个类型参数:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair<string, number>('age', 25); // 类型:[string, number]
泛型接口
泛型不仅可以用于函数,还可以用于接口:
interface Box<T> {
value: T;
getValue(): T;
}
const stringBox: Box<string> = {
value: 'Hello',
getValue() {
return this.value;
}
};
const numberBox: Box<number> = {
value: 42,
getValue() {
return this.value;
}
};
泛型类
同样,类也可以使用泛型:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
// 使用示例
const stringStack = new Stack<string>();
stringStack.push('Hello');
stringStack.push('World');
console.log(stringStack.pop()); // 'World'
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
console.log(numberStack.pop()); // 3
泛型约束
有时候,我们希望泛型参数满足某些条件,这时可以使用泛型约束。
使用 extends 关键字
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
// 可以使用,因为 string 有 length 属性
logLength('Hello'); // 输出:5
// 可以使用,因为数组有 length 属性
logLength([1, 2, 3]); // 输出:3
// 错误:number 没有 length 属性
// logLength(42);
在约束中使用类型参数
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: 'Alice', age: 30, city: 'Beijing' };
const name = getProperty(person, 'name'); // 类型:string
const age = getProperty(person, 'age'); // 类型:number
// 错误:'salary' 不是 person 的属性
// const salary = getProperty(person, 'salary');
实用工具类型中的泛型
TypeScript 内置了很多实用的工具类型,它们都使用了泛型。
Partial
将类型的所有属性变为可选:
interface User {
id: number;
name: string;
email: string;
}
// 使用 Partial
function updateUser(id: number, updates: Partial<User>): User {
// 实现更新逻辑
return { id, name: 'Alice', email: 'alice@example.com' };
}
// 可以只更新部分属性
updateUser(1, { name: 'Bob' });
updateUser(1, { email: 'bob@example.com' });
updateUser(1, { name: 'Charlie', email: 'charlie@example.com' });
Pick<T, K>
从类型中选择一组属性:
interface Product {
id: number;
name: string;
price: number;
description: string;
category: string;
}
// 只选择 id 和 name
type ProductSummary = Pick<Product, 'id' | 'name'>;
const summary: ProductSummary = {
id: 1,
name: 'Laptop'
};
Omit<T, K>
从类型中排除一组属性:
// 排除敏感信息
type PublicUser = Omit<User, 'password' | 'salt'>;
Record<K, T>
构造一个对象类型,其属性键为 K,属性值为 T:
// 创建一个映射类型
type RolePermissions = Record<'admin' | 'user' | 'guest', string[]>;
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read']
};
高级泛型技巧
条件类型
条件类型允许我们根据类型条件来选择不同的类型:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
映射类型
映射类型允许我们基于现有类型创建新类型:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
// 等价于:
// {
// readonly name: string;
// readonly age: number;
// }
推断类型参数
在条件类型中,可以使用 infer 关键字推断类型:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function getString(): string {
return 'hello';
}
type Result = ReturnType<typeof getString>; // string
实际应用场景
1. API 响应处理
在前端开发中,我们经常需要处理 API 响应。使用泛型可以让我们编写类型安全的响应处理代码:
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
name: string;
price: number;
}
// 获取用户列表
async function fetchUsers(): Promise<ApiResponse<User[]>> {
const response = await fetch('/api/users');
return response.json();
}
// 获取产品列表
async function fetchProducts(): Promise<ApiResponse<Product[]>> {
const response = await fetch('/api/products');
return response.json();
}
// 使用示例
async function loadData() {
const userResponse = await fetchUsers();
if (userResponse.success) {
console.log(userResponse.data[0].name); // 类型安全
}
const productResponse = await fetchProducts();
if (productResponse.success) {
console.log(productResponse.data[0].price); // 类型安全
}
}
2. 通用数据存储服务
假设我们需要创建一个通用的数据存储类,可以存储任何类型的数据:
class DataStore<T> {
private storage: Map<string, T> = new Map();
set(key: string, value: T): void {
this.storage.set(key, value);
}
get(key: string): T | undefined {
return this.storage.get(key);
}
has(key: string): boolean {
return this.storage.has(key);
}
delete(key: string): boolean {
return this.storage.delete(key);
}
clear(): void {
this.storage.clear();
}
size(): number {
return this.storage.size;
}
forEach(callback: (value: T, key: string) => void): void {
this.storage.forEach(callback);
}
}
// 使用示例
const userStore = new DataStore<User>();
userStore.set('user1', { id: 1, name: 'Alice', email: 'alice@example.com' });
userStore.set('user2', { id: 2, name: 'Bob', email: 'bob@example.com' });
const user1 = userStore.get('user1');
console.log(user1?.name); // 'Alice'
const productStore = new DataStore<Product>();
productStore.set('prod1', { id: 1, name: 'Laptop', price: 9999 });
3. 事件发射器
在构建事件驱动的应用时,泛型可以帮助我们创建类型安全的事件系统:
type EventHandler<T = any> = (data: T) => void;
class EventEmitter<T extends Record<string, any>> {
private handlers: {
[K in keyof T]?: EventHandler<T[K]>[];
} = {};
on<K extends keyof T>(event: K, handler: EventHandler<T[K]>): void {
if (!this.handlers[event]) {
this.handlers[event] = [];
}
this.handlers[event]!.push(handler);
}
off<K extends keyof T>(event: K, handler: EventHandler<T[K]>): void {
const handlers = this.handlers[event];
if (handlers) {
this.handlers[event] = handlers.filter(h => h !== handler);
}
}
emit<K extends keyof T>(event: K, data: T[K]): void {
const handlers = this.handlers[event];
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
}
// 定义事件类型
interface AppEvents {
userLogin: { userId: number; username: string };
userLogout: { userId: number };
dataUpdate: { table: string; records: number };
error: { message: string; code: number };
}
// 使用示例
const emitter = new EventEmitter<AppEvents>();
emitter.on('userLogin', (data) => {
console.log(`User ${data.username} logged in`);
});
emitter.on('error', (data) => {
console.error(`Error ${data.code}: ${data.message}`);
});
// 发射事件
emitter.emit('userLogin', { userId: 1, username: 'Alice' });
emitter.emit('error', { message: 'Something went wrong', code: 500 });
4. 表单验证器
泛型可以帮助我们创建可复用的表单验证逻辑:
interface ValidationRule<T> {
validate: (value: T) => boolean;
message: string;
}
interface FormField<T> {
value: T;
rules: ValidationRule<T>[];
error?: string;
}
class FormValidator<T extends Record<string, any>> {
private fields: {
[K in keyof T]: FormField<T[K]>;
} = {} as any;
addField<K extends keyof T>(
name: K,
initialValue: T[K],
rules: ValidationRule<T[K]>[] = []
): void {
this.fields[name] = {
value: initialValue,
rules
};
}
validateField<K extends keyof T>(name: K): boolean {
const field = this.fields[name];
if (!field) return true;
for (const rule of field.rules) {
if (!rule.validate(field.value)) {
field.error = rule.message;
return false;
}
}
field.error = undefined;
return true;
}
validateAll(): boolean {
let isValid = true;
for (const name in this.fields) {
if (!this.validateField(name as keyof T)) {
isValid = false;
}
}
return isValid;
}
getValues(): T {
const values = {} as T;
for (const name in this.fields) {
values[name as keyof T] = this.fields[name as keyof T].value;
}
return values;
}
}
// 使用示例
interface LoginForm {
username: string;
password: string;
email: string;
}
const loginValidator = new FormValidator<LoginForm>();
// 添加字段和验证规则
loginValidator.addField('username', '', [
{
validate: (v) => v.length >= 3,
message: '用户名至少需要 3 个字符'
}
]);
loginValidator.addField('password', '', [
{
validate: (v) => v.length >= 8,
message: '密码至少需要 8 个字符'
},
{
validate: (v) => /[A-Z]/.test(v),
message: '密码必须包含大写字母'
}
]);
loginValidator.addField('email', '', [
{
validate: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
message: '请输入有效的邮箱地址'
}
]);
// 设置值
loginValidator.fields.username.value = 'alice';
loginValidator.fields.password.value = 'Password123';
loginValidator.fields.email.value = 'alice@example.com';
// 验证
const isValid = loginValidator.validateAll();
console.log('表单验证结果:', isValid);
最佳实践与常见陷阱
1. 选择合适的类型参数名
虽然可以使用任何名称作为类型参数,但有一些约定俗成的命名:
- INLINE_CODE_1 - Type(最常用)
- INLINE_CODE_2 - Key
- INLINE_CODE_3 - Value
- INLINE_CODE_4 - Property
- INLINE_CODE_5 - Return type
- INLINE_CODE_6 - Element (常用于数组或集合)
对于多个类型参数,可以使用更有描述性的名称:
// 好
function merge<T, U>(obj1: T, obj2: U): T & U { ... }
// 更好
function merge<TObject, TExtension>(obj1: TObject, obj2: TExtension): TObject & TExtension { ... }
2. 避免过度使用泛型
泛型很强大,但不应该在每个地方都使用。如果类型是明确的,直接使用具体类型会更清晰:
// 不必要
function process<T>(data: string): T {
return data as T;
}
// 更好
function process(data: string): string {
return data;
}
3. 提供默认类型参数
对于库作者来说,提供默认类型参数可以提高易用性:
interface Cache<T = any> {
get(key: string): T | undefined;
set(key: string, value: T): void;
}
// 使用时可以不指定类型
const cache: Cache = {
get: (key) => undefined,
set: (key, value) => {}
};
// 也可以指定具体类型
const userCache: Cache<User> = {
get: (key) => undefined,
set: (key, value) => {}
};
4. 使用泛型约束提高类型安全
当泛型参数需要满足特定条件时,一定要使用约束:
// 不好 - 没有约束,可能导致运行时错误
function merge<T>(obj1: T, obj2: T): T {
return { ...obj1, ...obj2 }; // 如果 T 不是对象类型会出错
}
// 好 - 使用约束确保类型安全
function merge<T extends object>(obj1: T, obj2: T): T {
return { ...obj1, ...obj2 };
}
总结
泛型是 TypeScript 中最强大的特性之一,它让我们能够编写更加灵活、可复用且类型安全的代码。通过本文的学习,你应该已经掌握了:
- 泛型的基本概念和语法
- 泛型函数、接口和类的使用
- 泛型约束和条件类型
- 实用工具类型的应用
- 实际项目中的常见应用场景
- 最佳实践和常见陷阱
记住,掌握泛型需要实践。建议你在日常开发中有意识地使用泛型,从简单的场景开始,逐步应用到更复杂的情况中。随着时间的推移,你会发现泛型成为你编写高质量 TypeScript 代码的得力助手。
TypeScript 的类型系统非常强大,而泛型是其中的核心。当你能够熟练运用泛型时,你就真正迈入了 TypeScript 高级开发者的行列。继续学习,继续实践,你会发现类型编程的乐趣!
关于作者:本文作者是一位热爱技术分享的开发者,专注于 TypeScript 和前端工程化领域。欢迎在评论区交流讨论!