TypeScript系列04-泛型编程

server/2025/3/6 11:45:12/

本文探讨TypeScript泛型编程,内容涵盖:

  1. 泛型基础:包括泛型函数、接口、类和约束,这些是构建可重用和类型安全代码的基础。
  2. 内置工具类型:掌握了TypeScript提供的强大工具类型,如PartialRequiredPick等,这些工具类型可以帮助我们进行常见的类型操作。
  3. 条件类型与推断:学习了如何使用条件类型和infer关键字进行类型运算和推断。
  4. 实战应用:分析了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中的泛型带来的好处:

  1. 类型安全的Actions:通过PayloadAction<T>泛型,确保了action的载荷类型正确。
  2. 类型安全的ThunkscreateAsyncThunk<返回值类型, 参数类型, 选项>泛型确保了异步操作的类型安全。
  3. 类型安全的State访问:通过RootState类型和强类型的selector hooks,确保了状态的类型安全访问。
  4. 智能的自动完成:由于类型系统的存在,IDE可以提供更好的自动完成功能。
  5. 编译时错误检查:错误在编译时而非运行时被捕获。

这些高级泛型技术使Redux Toolkit能够提供卓越的开发体验,尤其在大型应用中尤为重要。

总结

泛型是TypeScript最强大的特性之一,掌握泛型可以帮助我们写出更灵活、更可重用、更类型安全的代码。随着TypeScript的不断发展,泛型的应用将变得越来越广泛和重要。通过深入理解泛型,我们可以充分利用TypeScript的类型系统,提高代码质量和开发效率。


http://www.ppmy.cn/server/172861.html

相关文章

容器 /dev/shm 泄漏学习

容器 /dev/shm 泄漏的介绍 在容器环境中&#xff0c;/dev/shm 是一个基于 tmpfs 的共享内存文件系统&#xff0c;通常用于进程间通信&#xff08;IPC&#xff09;和临时数据存储。由于其内存特性&#xff0c;/dev/shm 的大小是有限的&#xff0c;默认情况下 Docker 容器的 /de…

ASP.NET Core JWT认证与授权

1.JWT结构 JSON Web Token&#xff08;JWT&#xff09;是一种用于在网络应用之间安全传输声明的开放标准&#xff08;RFC 7519&#xff09;。它通常由三部分组成&#xff0c;以紧凑的字符串形式表示&#xff0c;在身份验证、信息交换等场景中广泛应用。 2.JWT权限认证 2.1添…

硬件工程师入门教程

1.欧姆定律 测电压并联使用万用表测电流串联使用万用表&#xff0c;红入黑出 2.电阻的阻值识别 直插电阻 贴片电阻 3.电阻的功率 4.电阻的限流作用 限流电阻阻值的计算 单位换算关系 5.电阻的分流功能 6.电阻的分压功能 7.电容 电容简单来说是两块不连通的导体加上中间的绝…

Linxu几种登陆方式介绍

Linux 系统除了传统的密码登陆方式外&#xff0c;还提供了多种其他的登陆方法&#xff0c;每种方法都有其独特的优点和缺点。了解这些不同的登陆方式可以帮助你根据自己的需求和安全考虑&#xff0c;选择最合适的登陆方案。以下是 Linux 系统中常见的几种密码登陆之外的登陆方式…

ESP32-P4 支持哪些 RISC-V 汇编指令?

RISC-V 采用模块化设计&#xff0c;指令集由多个扩展模块组成&#xff0c;最常见的包括&#xff1a; I&#xff08;Integer&#xff09;—— 基础整数指令集&#xff08;所有 RISC-V 处理器必备&#xff09;。M&#xff08;Multiply/Divide&#xff09;—— 乘法和除法指令。A…

Qt开发⑫Qt界面优化之CSS_选择器_控件样式

目录 1. CSS背景介绍 2. 基本语法 3. CSS 设置方式 3.1 指定控件样式设置 3.2 全局样式设置 3.3 从文件加载样式表 3.4 使用 Qt Designer 编辑样式 4. 选择器 4.1 选择器概况 4.2 子控件选择器 4.3 伪类选择器 5. 样式属性_盒模型 5.1 设置边框和内边距 5.2 设置…

el-select的下拉选择框插入el-checkbox

el-check注意这里要使用model-value绑定数据 <el-selectv-model"selectDevice"multiplecollapse-tags:multiple-limit"5"style"width: 200px"popper-class"select-popover-class" ><el-optionv-for"item in deviceList…

Web3 的未来:去中心化如何重塑互联网

Web3 的未来&#xff1a;去中心化如何重塑互联网 在这个信息爆炸的时代&#xff0c;我们正站在一个新的技术革命的门槛上——Web3。Web3 不仅仅是一个技术术语&#xff0c;它代表了一种全新的互联网理念&#xff0c;即去中心化。这种理念正在逐步改变我们对互联网的使用方式和…