本文探讨TypeScript泛型编程,内容涵盖:
- 泛型基础:包括泛型函数、接口、类和约束,这些是构建可重用和类型安全代码的基础。
- 内置工具类型:掌握了TypeScript提供的强大工具类型,如
Partial
、Required
、Pick
等,这些工具类型可以帮助我们进行常见的类型操作。 - 条件类型与推断:学习了如何使用条件类型和
infer
关键字进行类型运算和推断。 - 实战应用:分析了Redux Toolkit中泛型的应用,展示了泛型在实际项目中的强大功能。
1. 泛型基础概念
泛型是TypeScript中最强大的特性之一,它允许我们创建可重用的组件,这些组件可以与多种类型一起工作,而不仅限于单一类型。泛型为代码提供了类型安全的同时保持了灵活性。
1.1 泛型函数与泛型接口
泛型函数使用类型参数来创建可以处理多种数据类型的函数,同时保持类型安全。
以下是一个简单的泛型函数示例:
typescript">function identity<T>(arg: T): T {return arg;
}// 使用方式
const output1: string = identity<string>("hello");
const output2: number = identity<number>(42);
const output3: boolean = identity(true); // 类型参数推断为 boolean
泛型接口使我们能够定义可适用于多种类型的接口结构:
typescript">interface GenericBox<T> {value: T;getValue(): T;
}// 实现泛型接口
class StringBox implements GenericBox<string> {value: string;constructor(value: string) {this.value = value;}getValue(): string {return this.value;}
}class NumberBox implements GenericBox<number> {value: number;constructor(value: number) {this.value = value;}getValue(): number {return this.value;}
}
1.2 泛型类与泛型约束
泛型类允许我们创建可以处理多种数据类型的类定义:
typescript">class DataContainer<T> {private data: T[];constructor() {this.data = [];}add(item: T): void {this.data.push(item);}getItems(): T[] {return this.data;}
}// 使用泛型类
const stringContainer = new DataContainer<string>();
stringContainer.add("Hello");
stringContainer.add("World");
const strings = stringContainer.getItems(); // 类型为 string[]const numberContainer = new DataContainer<number>();
numberContainer.add(10);
numberContainer.add(20);
const numbers = numberContainer.getItems(); // 类型为 number[]
泛型约束使我们可以限制类型参数必须具有特定属性或结构,提高类型安全性:
typescript">interface Lengthwise {length: number;
}// 泛型约束:T 必须符合 Lengthwise 接口
function getLength<T extends Lengthwise>(arg: T): number {return arg.length; // 安全,因为我们保证 T 有 length 属性
}getLength("Hello"); // 字符串有 length 属性,可以正常工作
getLength([1, 2, 3]); // 数组有 length 属性,可以正常工作
// getLength(123); // 错误!数字没有 length 属性
1.3 默认类型参数
TypeScript 允许为泛型类型参数提供默认值,类似于函数参数的默认值:
typescript">interface ApiResponse<T = any> {data: T;status: number;message: string;
}// 没有指定类型参数,使用默认值 any
const generalResponse: ApiResponse = {data: "some data",status: 200,message: "Success"
};// 明确指定类型参数
const userResponse: ApiResponse<User> = {data: { id: 1, name: "John Doe" },status: 200,message: "User retrieved successfully"
};interface User {id: number;name: string;
}
2. 泛型工具类型详解
TypeScript 提供了许多内置的泛型工具类型,它们可以帮助我们执行常见的类型转换。这些工具类型都是基于泛型构建的,展示了泛型的强大功能。
2.1 Partial, Required, Readonly
这组工具类型主要用于修改对象类型的属性特性:
typescript">interface User {id: number;name: string;email: string;role: 'admin' | 'user';createdAt: Date;
}// Partial<T> - 将所有属性变为可选
type PartialUser = Partial<User>;
// 等同于:
// {
// id?: number;
// name?: string;
// email?: string;
// role?: 'admin' | 'user';
// createdAt?: Date;
// }// 更新用户时,我们只需要提供要更新的字段
function updateUser(userId: number, userData: Partial<User>): Promise<User> {// 实现省略return Promise.resolve({} as User);
}// Required<T> - 将所有可选属性变为必需
interface PartialConfig {host?: string;port?: number;protocol?: 'http' | 'https';
}type CompleteConfig = Required<PartialConfig>;
// 等同于:
// {
// host: string;
// port: number;
// protocol: 'http' | 'https';
// }// Readonly<T> - 将所有属性变为只读
type ReadonlyUser = Readonly<User>;
// 等同于:
// {
// readonly id: number;
// readonly name: string;
// readonly email: string;
// readonly role: 'admin' | 'user';
// readonly createdAt: Date;
// }const user: ReadonlyUser = {id: 1,name: "John Doe",email: "john@example.com",role: "user",createdAt: new Date()
};// 错误:无法分配到"name",因为它是只读属性
// user.name = "Jane Doe";
2.2 Record<K,T>, Pick<T,K>, Omit<T,K>
这组工具类型主要用于构造或提取对象类型:
typescript">// Record<K,T> - 创建一个具有类型 K 的键和类型 T 的值的对象类型
type UserRoles = Record<string, 'admin' | 'editor' | 'viewer'>;
// 等同于:
// {
// [key: string]: 'admin' | 'editor' | 'viewer'
// }const roles: UserRoles = {'user1': 'admin','user2': 'editor','user3': 'viewer'
};// 特别有用的情况:创建映射对象
type UserIds = 'user1' | 'user2' | 'user3';
const permissionsByUser: Record<UserIds, string[]> = {user1: ['read', 'write', 'delete'],user2: ['read', 'write'],user3: ['read']
};// Pick<T,K> - 从类型 T 中选择指定的属性 K
type UserProfile = Pick<User, 'name' | 'email'>;
// 等同于:
// {
// name: string;
// email: string;
// }// 非常适合生成表单或API相关的数据结构
function getUserProfile(user: User): UserProfile {return {name: user.name,email: user.email};
}// Omit<T,K> - 从类型 T 中排除指定的属性 K
type UserWithoutSensitiveInfo = Omit<User, 'id' | 'createdAt'>;
// 等同于:
// {
// name: string;
// email: string;
// role: 'admin' | 'user';
// }// 创建新用户输入表单,去除自动生成的字段
function createUserFromForm(userData: UserWithoutSensitiveInfo): User {return {...userData,id: generateId(), // 假设的函数createdAt: new Date()};
}
2.3 Extract<T,U>, Exclude<T,U>, NonNullable
这组工具类型主要用于联合类型的操作:
typescript">// 定义一些联合类型
type Species = 'cat' | 'dog' | 'bird' | 'fish' | 'reptile';
type Mammals = 'cat' | 'dog';// Extract<T,U> - 从 T 中提取可赋值给 U 的类型
type MammalsFromSpecies = Extract<Species, Mammals>;
// 结果: 'cat' | 'dog'// 更实用的例子
type ApiResponse = | { status: 'success'; data: any }| { status: 'error'; error: string }| { status: 'loading' };type SuccessResponse = Extract<ApiResponse, { status: 'success' }>;
// 结果: { status: 'success'; data: any }// Exclude<T,U> - 从 T 中排除可赋值给 U 的类型
type NonMammals = Exclude<Species, Mammals>;
// 结果: 'bird' | 'fish' | 'reptile'// 排除所有错误状态
type NonErrorResponses = Exclude<ApiResponse, { status: 'error' }>;
// 结果: { status: 'success'; data: any } | { status: 'loading' }// NonNullable<T> - 从 T 中排除 null 和 undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;
// 结果: string// 使用场景:过滤数组中的非空值
function filterNonNullable<T>(array: Array<T | null | undefined>): Array<NonNullable<T>> {return array.filter((item): item is NonNullable<T> => item !== null && item !== undefined) as Array<NonNullable<T>>;
}const mixedArray = ['hello', null, 'world', undefined, '!'];
const filteredArray = filterNonNullable(mixedArray);
// 结果: ['hello', 'world', '!']
3. 条件类型与类型推断 - infer 关键字
条件类型是TypeScript中最强大的类型构造之一,它允许我们基于类型关系创建条件逻辑。
infer
关键字,它允许我们声明一个类型变量,用于捕获和提取符合特定模式的类型。简单来说,它让我们能够从复杂类型中"提取"出我们关心的部分。
基本语法
typescript">type ExtractSomething<T> = T extends Pattern_with_infer_X ? X : Fallback;
在这个模式中:
T
是我们要检查的类型Pattern_with_infer_X
是包含infer X
声明的模式- 如果
T
符合该模式,结果类型就是我们提取出的X
- 否则,结果类型为
Fallback
简单示例:提取函数返回类型
typescript">// 定义一个提取函数返回类型的工具类型
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : any;// 一个简单的函数
function getUsername(): string {return "张三";
}// 提取函数的返回类型
type Username = ReturnTypeOf<typeof getUsername>; // 结果: string
infer
的本质是进行模式匹配。就像我们识别文字或图像一样,它根据预设的模式来找到并"捕获"类型中的特定部分。
这就好像我们看到"159****1234"这样的号码,立刻就能识别出这是一个手机号,并且知道中间的星号部分是隐藏的数字。infer
在类型世界做的事情与此类似——它根据上下文模式自动推断出被省略或隐藏的类型部分。
4. 案例:泛型在Redux Toolkit中的应用
Redux Toolkit是Redux的官方推荐工具集,它大量使用了TypeScript的泛型来提供类型安全的状态管理。让我们看看它如何利用泛型:
下面我们将看看Redux Toolkit中的泛型应用,并实现一个简单的TodoList应用:
typescript">import { createSlice, createAsyncThunk, PayloadAction, configureStore
} from '@reduxjs/toolkit';// 1. 定义类型
interface Todo {id: number;text: string;completed: boolean;
}interface TodosState {items: Todo[];status: 'idle' | 'loading' | 'succeeded' | 'failed';error: string | null;
}// 2. 使用createAsyncThunk泛型
// createAsyncThunk<返回值类型, 参数类型, { rejectValue: 错误类型 }>
export const fetchTodos = createAsyncThunkTodo[], void, { rejectValue: string }
>('todos/fetchTodos', async (_, { rejectWithValue }) => {try {const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=10');if (!response.ok) {return rejectWithValue('Failed to fetch todos.');}return await response.json();} catch (error) {return rejectWithValue(error instanceof Error ? error.message : 'Unknown error');}
});// 3. 使用createSlice泛型来创建切片
// createSlice<状态类型>
const todosSlice = createSlice({name: 'todos',initialState: {items: [],status: 'idle',error: null} as TodosState,reducers: {// PayloadAction<载荷类型> 增强了action的类型安全addTodo: (state, action: PayloadAction<string>) => {const newTodo: Todo = {id: Date.now(),text: action.payload,completed: false};state.items.push(newTodo);},toggleTodo: (state, action: PayloadAction<number>) => {const todo = state.items.find(item => item.id === action.payload);if (todo) {todo.completed = !todo.completed;}},removeTodo: (state, action: PayloadAction<number>) => {state.items = state.items.filter(item => item.id !== action.payload);}},extraReducers: (builder) => {// 处理异步action状态builder.addCase(fetchTodos.pending, (state) => {state.status = 'loading';}).addCase(fetchTodos.fulfilled, (state, action: PayloadAction<Todo[]>) => {state.status = 'succeeded';state.items = action.payload;}).addCase(fetchTodos.rejected, (state, action) => {state.status = 'failed';state.error = action.payload || 'Unknown error';});}
});// 4. 导出actions
export const { addTodo, toggleTodo, removeTodo } = todosSlice.actions;// 5. 配置store
const store = configureStore({reducer: {todos: todosSlice.reducer}
});// 6. 从store中提取RootState和AppDispatch类型
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;// 7. 强类型的Hooks
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';// 为useDispatch和useSelector创建强类型的版本
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;// 8. 在组件中使用
import React, { useEffect, useState } from 'react';
import { useAppDispatch, useAppSelector } from './store';
import { addTodo, toggleTodo, removeTodo, fetchTodos } from './todosSlice';const TodoApp: React.FC = () => {const [newTodo, setNewTodo] = useState('');const dispatch = useAppDispatch();// 强类型的selector,IDE可以提供自动完成const { todos, status, error} = useAppSelector(state => state.todos);useEffect(() => {if (status === 'idle') {dispatch(fetchTodos());}}, [status, dispatch]);const handleAddTodo = (e: React.FormEvent) => {e.preventDefault();if (newTodo.trim()) {dispatch(addTodo(newTodo));setNewTodo('');}};if (status === 'loading') {return <div>Loading...</div>;}if (status === 'failed') {return <div>Error: {error}</div>;}return (<div><h1>Todo List</h1><form onSubmit={handleAddTodo}><inputtype="text"value={newTodo}onChange={(e) => setNewTodo(e.target.value)}placeholder="Add a new todo"/><button type="submit">Add</button></form><ul>{todos.map(todo => (<likey={todo.id}style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}><span onClick={() => dispatch(toggleTodo(todo.id))}>{todo.text}</span><button onClick={() => dispatch(removeTodo(todo.id))}>Delete</button></li>))}</ul></div>);
};export default TodoApp;
Redux Toolkit中的泛型带来的好处:
- 类型安全的Actions:通过
PayloadAction<T>
泛型,确保了action的载荷类型正确。 - 类型安全的Thunks:
createAsyncThunk<返回值类型, 参数类型, 选项>
泛型确保了异步操作的类型安全。 - 类型安全的State访问:通过
RootState
类型和强类型的selector hooks,确保了状态的类型安全访问。 - 智能的自动完成:由于类型系统的存在,IDE可以提供更好的自动完成功能。
- 编译时错误检查:错误在编译时而非运行时被捕获。
这些高级泛型技术使Redux Toolkit能够提供卓越的开发体验,尤其在大型应用中尤为重要。
总结
泛型是TypeScript最强大的特性之一,掌握泛型可以帮助我们写出更灵活、更可重用、更类型安全的代码。随着TypeScript的不断发展,泛型的应用将变得越来越广泛和重要。通过深入理解泛型,我们可以充分利用TypeScript的类型系统,提高代码质量和开发效率。