TypeScript系列04-泛型编程
本文探讨TypeScript泛型编程,内容涵盖:
- 泛型基础:包括泛型函数、接口、类和约束,这些是构建可重用和类型安全代码的基础。
- 内置工具类型:掌握了TypeScript提供的强大工具类型,如
Partial
、Required
、Pick
等,这些工具类型可以帮助我们进行常见的类型操作。 - 条件类型与推断:学习了如何使用条件类型和
infer
关键字进行类型运算和推断。 - 实战应用:分析了Redux Toolkit中泛型的应用,展示了泛型在实际项目中的强大功能。
1. 泛型基础概念
泛型是TypeScript中最强大的特性之一,它允许我们创建可重用的组件,这些组件可以与多种类型一起工作,而不仅限于单一类型。泛型为代码提供了类型安全的同时保持了灵活性。
1.1 泛型函数与泛型接口
泛型函数使用类型参数来创建可以处理多种数据类型的函数,同时保持类型安全。
以下是一个简单的泛型函数示例:
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
泛型接口使我们能够定义可适用于多种类型的接口结构:
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 泛型类与泛型约束
泛型类允许我们创建可以处理多种数据类型的类定义:
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[]
泛型约束使我们可以限制类型参数必须具有特定属性或结构,提高类型安全性:
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 允许为泛型类型参数提供默认值,类似于函数参数的默认值:
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
这组工具类型主要用于修改对象类型的属性特性:
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>
这组工具类型主要用于构造或提取对象类型:
// 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
这组工具类型主要用于联合类型的操作:
// 定义一些联合类型
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
关键字,它允许我们声明一个类型变量,用于捕获和提取符合特定模式的类型。简单来说,它让我们能够从复杂类型中"提取"出我们关心的部分。
基本语法
type ExtractSomething<T> = T extends Pattern_with_infer_X ? X : Fallback;
在这个模式中:
T
是我们要检查的类型Pattern_with_infer_X
是包含infer X
声明的模式- 如果
T
符合该模式,结果类型就是我们提取出的X
- 否则,结果类型为
Fallback
简单示例:提取函数返回类型
// 定义一个提取函数返回类型的工具类型
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应用:
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 = createAsyncThunk
Todo[],
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}>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a new todo"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li
key={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的类型系统,提高代码质量和开发效率。