当前位置: 首页 > article >正文

TypeScript系列04-泛型编程

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

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

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

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

总结

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


http://www.kler.cn/a/573616.html

相关文章:

  • CRC算法(C语言)
  • DApp开发从入门到精通:以太坊/Solana公链生态实战解析
  • 【长安大学】苹果手机/平板自动连接认证CHD-WIFI脚本(快捷指令)
  • Scala 中的数据类型
  • Docker--Docker 镜像制作
  • HTML第三节
  • AI-Deepseek + PPT
  • RAG检索增强生成(Retrieval-Augmented Generation)介绍(双模态架构:检索子系统、生成子系统)实现知识获取与内容生成的协同
  • C#使用SFTP批量上传和下载一个目录下的所有文件
  • Linux下启动redis
  • 8.RabbitMQ队列详解
  • java数据结构_再谈String_10
  • 15Metasploit框架介绍
  • 如何同步this.goodAllData里面的每一项给到row
  • 【Flink银行反欺诈系统设计方案】4.Flink CEP 规则表刷新方式
  • 图像伽马矫正 + 亮度调整 + 对比度调整
  • Redis面试常见问题——集群方案
  • Hi3516CV610电瓶车检测 电动自行车检测 人脸检测 人形检测 车辆检测 宠物检测 包裹检测 源码
  • Win10 用户、组与内置安全主体概念详解
  • Android中的触摸事件是如何传递和处理的