Typescript 泛型
Typescript 泛型
一、讲一讲什么是泛型
TypeScript 中的泛型是一种强大的工具,它允许你在定义函数、类或接口时使用类型变量,这些类型变量可以在使用时被具体的类型所替代。泛型的主要作用是创建可重用的组件,同时保持类型安全。
1.1 为什么需要泛型?
假设你需要创建一个函数,它返回传入的任何值。在不使用泛型的情况下,你可能会这样写:
function identity(arg: any): any {return arg;
}
虽然这个函数可以工作,但它失去了类型信息。如果传入一个 number,返回值的类型是 any,而不是 number。泛型可以解决这个问题。
1.2 泛型函数
泛型函数使用类型变量(通常用 T 表示)来捕获调用时传入的类型:
function identity<T>(arg: T): T {return arg;
}// 使用方式
const num = identity<number>(123); // 指定 T 为 number
const str = identity("hello"); // 类型推断,T 为 string
- 类型变量
T
:代表任意类型,在调用时确定具体类型。 - 类型推断:可以省略
<number>
,TypeScript
会自动推断类型。
1.3 泛型接口
泛型可以用于接口定义:
interface GenericIdentityFn<T> {(arg: T): T;
}const myIdentity: GenericIdentityFn<number> = identity;
这里 GenericIdentityFn<T>
是一个泛型接口,它接受一个类型参数 T
。
1.4 泛型类
泛型类的语法类似泛型接口:
class GenericNumber<T> {zeroValue: T;add: (x: T, y: T) => T;constructor(zeroValue: T, add: (x: T, y: T) => T) {this.zeroValue = zeroValue;this.add = add;}
}// 使用示例
const numGenerator = new GenericNumber<number>(0, (x, y) => x + y);
const strGenerator = new GenericNumber<string>("", (x, y) => x + y);
1.5 泛型约束
有时你需要限制泛型的类型范围,例如要求类型必须具有某个属性:
interface Lengthwise {length: number;
}function loggingIdentity<T extends Lengthwise>(arg: T): T {console.log(arg.length); // 现在可以访问 length 属性return arg;
}loggingIdentity("hello"); // 合法,string 有 length 属性
loggingIdentity([1, 2, 3]); // 合法,数组有 length 属性
// loggingIdentity(123); // 错误,number 没有 length 属性
1.6 泛型参数的默认类型
可以为泛型参数指定默认类型:
function createArray<T = number>(length: number, value: T): T[] {return Array(length).fill(value);
}const numbers = createArray(3, 0); // T 默认为 number
const strings = createArray<string>(3, "x"); // 指定 T 为 string
1.7 泛型工具类型
TypeScript 内置了一些常用的泛型工具类型:
Partial<T>
:将类型T
的所有属性变为可选。Readonly<T>
:将类型T
的所有属性变为只读。Pick<T, K>
:从类型T
中选取部分属性K
。Exclude<T, U>
:从T
中排除可以赋值给U
的类型。
type User = { name: string; age: number; email: string };type PartialUser = Partial<User>; // { name?: string; age?: number; email?: string }
type ReadonlyUser = Readonly<User>; // { readonly name: string; ... }
type NameAndAge = Pick<User, "name" | "age">; // { name: string; age: number }
二、泛型在实际开发中有哪些常见的应用场景
2.1 数据结构与容器
泛型常用于实现通用的数据结构,如数组、链表、栈、队列等,使其可以存储任意类型的数据,同时保持类型安全。
示例:泛型数组
// 内置的 Array<T> 是泛型
const numbers: Array<number> = [1, 2, 3];
const strings: Array<string> = ["a", "b", "c"];// 自定义泛型容器
class Stack<T> {private items: T[] = [];push(item: T) {this.items.push(item);}pop(): T | undefined {return this.items.pop();}
}const numberStack = new Stack<number>();
numberStack.push(1);
const num = numberStack.pop(); // 类型为 number
示例:泛型链表
class LinkedListNode<T> {value: T;next: LinkedListNode<T> | null;constructor(value: T, next: LinkedListNode<T> | null = null) {this.value = value;this.next = next;}
}const numberNode = new LinkedListNode<number>(1); // 类型为 number
示例:泛型队列
class Queue<T> {private items: T[] = [];enqueue(item: T) {this.items.push(item);}dequeue(): T | undefined {return this.items.shift();}
}const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
const num = numberQueue.dequeue(); // 类型为 number
2.2 API 请求与响应处理
在前后端交互中,API 返回的数据结构通常是通用的,但具体类型不同。泛型可以确保响应数据的类型安全。
示例:通用 API 响应类型
interface ApiResponse<T> {code: number;message: string;data: T;
}// 获取用户列表
function fetchUsers(): Promise<ApiResponse<User[]>> {return fetch("/api/users").then(res => res.json());
}// 获取单个用户
function fetchUser(id: string): Promise<ApiResponse<User>> {return fetch(`/api/users/${id}`).then(res => res.json());
}
2.3 高阶组件与函数
泛型可用于创建复用性高的高阶组件或工具函数,这些组件 / 函数可以处理多种类型的数据。
示例:通用状态管理钩子
import { useState } from "react";function useLocalStorage<T>(key: string, initialValue: T) {const [value, setValue] = useState<T>(() => {const stored = localStorage.getItem(key);return stored ? JSON.parse(stored) : initialValue;});const setStoredValue = (newValue: T | ((prev: T) => T)) => {const finalValue = typeof newValue === "function" ? (newValue as (prev: T) => T)(value) : newValue;localStorage.setItem(key, JSON.stringify(finalValue));setValue(finalValue);};return [value, setStoredValue] as const;
}// 使用方式
const [user, setUser] = useLocalStorage<User>("currentUser", { name: "", age: 0 });
2.4 类型安全的工具函数
泛型可以让工具函数适用于多种类型,同时保持类型检查。
示例:类型安全的深拷贝函数
function deepClone<T>(obj: T): T {return JSON.parse(JSON.stringify(obj));
}const original = { name: "Alice", age: 30 };
const clone = deepClone(original); // 类型为 { name: string; age: number }
2.5 接口与抽象类的多态实现
泛型可以让接口或抽象类在实现时支持不同的具体类型。
示例:泛型仓储接口
interface Repository<T> {findById(id: string): Promise<T | null>;save(entity: T): Promise<T>;delete(id: string): Promise<void>;
}// 用户仓储实现
class UserRepository implements Repository<User> {async findById(id: string) {// ...}// ...
}
2.6 事件系统与发布 - 订阅模式
泛型可以确保事件数据的类型一致性。
示例:泛型事件总线
type EventMap = Record<string, any>;class EventEmitter<T extends EventMap> {private events: Partial<{ [K in keyof T]: Array<(data: T[K]) => void> }> = {};on<K extends keyof T>(event: K, listener: (data: T[K]) => void) {if (!this.events[event]) this.events[event] = [];this.events[event]?.push(listener);}emit<K extends keyof T>(event: K, data: T[K]) {this.events[event]?.forEach(listener => listener(data));}
}// 使用方式
type AppEvents = {"user:created": User;"order:placed": Order;
};const emitter = new EventEmitter<AppEvents>();
emitter.on("user:created", (user) => {// user 类型为 User
});
emitter.emit("order:placed", { id: "123", amount: 99.99 }); // 类型检查
2.7 泛型工具类型的应用
TypeScript
内置的泛型工具类型(如 Partial<T>
、Pick<T, K>
)在实际开发中经常用于类型转换。
示例:表单数据处理
type User = {id: string;name: string;age: number;email: string;
};// 表单数据通常是可选的
type UserFormData = Partial<User>;// 从 User 中选取部分字段
type UserPublicInfo = Pick<User, "name" | "email">;
三、 泛型的类型变量的作用
在 TypeScript 中,泛型的类型变量(如 T、U、K 等)是泛型系统的核心机制,它们允许你创建可复用的组件,同时保持类型安全。以下是类型变量的详细作用和应用场景:
3.1 捕获调用时的类型
类型变量的主要作用是捕获用户使用泛型时传入的具体类型,并在整个泛型定义中使用该类型。
示例:
function identity<T>(arg: T): T {return arg;
}const num = identity<number>(123); // T 被捕获为 number
const str = identity("hello"); // 类型推断:T 为 string
T
捕获了传入参数的类型,并将其作为返回值的类型。
3.2 实现类型参数化
类型变量使函数、类或接口能够接受任意类型的参数,从而实现代码复用。
示例:泛型数组处理函数
function firstElement<T>(arr: T[]): T | undefined {return arr[0];
}const nums = firstElement([1, 2, 3]); // T 为 number,返回值类型为 number | undefined
const strs = firstElement(["a", "b"]); // T 为 string,返回值类型为 string | undefined
- 函数可以处理任意类型的数组,而不需要为每种类型单独实现。
3.3 类型变量的约束与关联
类型变量可以相互约束,确保类型之间的一致性。
示例:泛型键值对
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {return obj[key];
}const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // T 为 User,K 为 "name",返回值类型为 string
// const invalid = getProperty(user, "email"); // 错误:"email" 不是 User 的键
K extends keyof T
约束K
必须是T
的键之一。
3.4 泛型类与接口的类型变量
类型变量可以用于类或接口的定义,使其能够处理不同类型的数据。
示例:泛型字典
interface Dictionary<T> {[key: string]: T;
}const numbers: Dictionary<number> = { a: 1, b: 2 };
const strings: Dictionary<string> = { hello: "world" };
Dictionary<T>
接口可以存储任意类型的值,但所有值的类型必须一致。
3.5 类型变量的默认值
可以为类型变量指定默认类型,使泛型在使用时更加灵活。
示例:带默认类型的泛型
function createArray<T = number>(length: number, value: T): T[] {return Array(length).fill(value);
}const numbers = createArray(3, 0); // T 默认为 number
const strings = createArray<string>(3, "x"); // 指定 T 为 string
3.6 类型变量的作用域
示例:
function identity<T>(arg: T): T {// T 仅在函数内部有效return arg;
}// T 在此处不存在
3.7 多个类型变量的协作
泛型可以使用多个类型变量,它们之间可以相互关联或约束。
示例:泛型映射函数
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {return arr.map(fn);
}const numbers = [1, 2, 3];
const strings = map(numbers, (n) => n.toString()); // T 为 number,U 为 string
T
表示输入数组的元素类型,U
表示映射后的元素类型。
3.6 类型变量在工具类型中的应用
TypeScript
内置的工具类型(如 Partial<T>
、Exclude<T, U>
)大量使用泛型类型变量。
示例:
type Partial<T> = {[P in keyof T]?: T[P];
};type User = { name: string; age: number };
type PartialUser = Partial<User>; // { name?: string; age?: number }
T
是传入的原始类型,P
是遍历T
的键。
四、泛型在Vue中的应用
在 Vue 3
(结合 TypeScript
)中,泛型的应用场景非常广泛,它可以显著提升类型安全性和代码复用性。
4.1 组件 props
和 emits
的类型定义
泛型可用于精确指定 props
和 emits
的类型,避免手动重复定义类型。
4.1.1 泛型 props
定义
使用 DefineComponent
或 defineComponent
时,可以通过泛型参数指定 props
类型:
import { defineComponent } from 'vue';interface Props {message: string;count: number;
}const MyComponent = defineComponent<Props>({props: {message: { type: String, required: true },count: { type: Number, default: 0 },},setup(props) {// props.message 类型为 string// props.count 类型为 number},
});
4.1.2 泛型 emits
定义
使用 defineEmits
时,可以通过泛型参数指定事件类型:
const MyComponent = defineComponent({emits: ['change', 'submit'] as const,setup(props, { emit }) {// 使用泛型约束 emit 参数类型const handleChange = (value: string) => {emit('change', value); // 类型检查:value 必须为 string};},
});
4.2 ref
、reactive
和 computed
的类型推导
泛型可用于明确指定响应式数据的类型,特别是在初始值为 null
或需要更精确类型时。
4.2.1 ref
的泛型用法
import { ref, Ref } from 'vue';// 明确指定 ref 类型
const count = ref<number | null>(null); // count.value 类型为 number | null
count.value = 123; // 类型检查通过// 复杂类型
interface User {name: string;age: number;
}// 明确指定 ref 类型为 User
const user = ref<User>({ name: 'Alice', age: 30 }); // user.value 类型为 User
4.2.2 reactive
的泛型用法
reactive
会自动推导对象的类型,并返回一个响应式版本:
import { reactive } from 'vue';// 自动推导类型
const state = reactive({name: 'Alice',age: 30,address: {city: 'Beijing',street: '123 Main St'}
});// 类型推导结果:
// state: {
// name: string;
// age: number;
// address: {
// city: string;
// street: string;
// };
// }
interface User {name: string;age: number;hobbies?: string[];
}const user = reactive<User>({name: 'Bob',age: 25,hobbies: ['reading', 'swimming']
});// 类型检查:
user.name = 123; // 错误:不能将 number 赋值给 string
user.hobbies?.push('running'); // 正确
4.2.3 computed
的泛型用法
import { computed, ComputedRef } from 'vue';const count = ref(0);
const double: ComputedRef<number> = computed(() => count.value * 2);
4.3 组合式函数(Composables)的泛型设计
泛型使组合式函数可以处理多种类型的数据,增强复用性。
4.3.1 通用状态管理
import { ref, Ref } from 'vue';
// 通用状态管理
function useLocalStorage<T>(key: string, initialValue: T) {// 从本地存储获取初始值const storedValue = localStorage.getItem(key);// 类型断言const initialValueWithType: T = storedValue ? JSON.parse(storedValue) : initialValue;// 类型断言const value = ref<T>(initialValueWithType) as Ref<T>;const setValue = (newValue: T) => {value.value = newValue; // 类型检查:newValue 必须为 TlocalStorage.setItem(key, JSON.stringify(newValue)); // 类型检查:newValue 必须为 T};return { value, setValue };
}// 使用方式
const { value: user } = useLocalStorage<User>('user', { name: '', age: 0 });// user.value 类型为 User
4.3.2 通用 API 请求
import { ref, Ref } from 'vue';
import axios from 'axios';interface ApiResponse<T> {data: T;message: string;
}interface User {name: string;age: number;
}
// 通用 API 请求
function useApi<T>(url: string) {const data = ref<T | null>(null); // data.value 类型为 T | nullconst loading = ref(true); // loading.value 类型为 booleanconst error = ref<string | null>(null); // error.value 类型为 string | null// 类型断言const dataWithType = data as Ref<T>;// 类型断言const loadingWithType = loading as Ref<boolean>;// 类型断言const errorWithType = error as Ref<string | null>;// 类型断言const fetchData = async () => { try {const response = await axios.get<ApiResponse<T>>(url); // 类型检查:response.data 必须为 ApiResponse<T>dataWithType.value = response.data.data; // 类型检查:response.data.data 必须为 T } catch (err) {errorWithType.value = err.message; // 类型检查:err.message 必须为 string} finally {loadingWithType.value = false; // 类型检查:loadingWithType.value 必须为 boolean}};return { data, loading, error, fetchData };
}// 使用方式
const { data, loading, error, fetchData } = useApi<User[]>('/api/users');// data.value 类型为 User[] | null
4.4 自定义指令的泛型
泛型可用于约束自定义指令绑定值的类型。
import { Directive } from 'vue';interface ScrollOptions {threshold: number; // 类型检查:threshold 必须为 numbercallback: () => void; // 类型检查:callback 必须为函数}
// 自定义指令的泛型
const vScroll: Directive<HTMLElement, ScrollOptions> = { mounted(el, binding) {// binding.value 类型为 ScrollOptionsconst options = binding.value; // ...},
};
4.5 Vue Router 的类型安全
Vue Router 4
结合 TypeScript
使用泛型增强路由参数和查询的类型安全。
4.5.1 路由参数类型
import { createRouter, createWebHistory } from 'vue-router';
import { useRoute } from 'vue-router';
// 路由参数类型
interface RouteParams {id: string;
}
// 路由配置类型
const router = createRouter({history: createWebHistory(), // 路由历史模式// 路由配置routes: [{path: '/user/:id', // 路由路径name: 'User', // 路由名称component: UserComponent, // 路由组件},],
});// 在组件中使用
const route = useRoute<RouteParams>(); // 类型检查:route.params 必须为 RouteParams
const userId = route.params.id; // 类型为 string
4.5.2 导航守卫的泛型
// 路由查询类型
interface RouteQuery {name: string;
}// 路由组件类型
interface RouteComponent {name: string;
}// 路由配置类型
interface RouteConfig {path: string;name: string;component: RouteComponent;
}// 导航守卫的泛型
router.beforeEach((to, from) => {// to.params 和 from.params 的类型会根据路由定义自动推导// 类型检查:to.params 必须为 RouteParams// 类型检查:from.params 必须为 RouteParams// 类型检查:to.query 必须为 RouteQuery// 类型检查:from.query 必须为 RouteQuery// 类型检查:to.component 必须为 RouteComponent// 类型检查:from.component 必须为 RouteComponent// 类型检查:to 必须为 RouteConfig// 类型检查:from 必须为 RouteConfigreturn true;
});
4.6 Vuex/Pinia
状态管理
泛型可用于增强状态管理库的类型安全。
4.6.1 Pinia Store
import { defineStore } from 'pinia';
// `Pinia Store` 类型
interface State {items: string[]; // 类型检查:items 必须为 string[]loading: boolean; // 类型检查:loading 必须为 boolean
}// `Pinia Store` 实例
const useMyStore = defineStore<string, State>('myStore', {// `Pinia Store` 状态state: () => ({ items: [], // 类型检查:items 必须为 string[]loading: false, // 类型检查:loading 必须为 boolean}),// `Pinia Store` 方法actions: {// `Pinia Store` 方法类型addItem(item: string) {this.items.push(item); // 类型检查:item 必须为 string},},
});
4.7 插件和高阶组件的泛型设计
泛型可用于创建适用于多种组件类型的插件或高阶组件。
4.7.1 高阶组件(HOC
)
import { Component, defineComponent } from 'vue';
import { ref } from 'vue';// 高阶组件(`HOC`)类型
function withLoading<T extends Component>(Component: T) {// 高阶组件(`HOC`)类型return defineComponent({components: { Component }, // 类型检查:Component 必须为 T// 高阶组件(`HOC`)属性props: {loading: {type: Boolean, // 类型检查:loading 必须为 booleandefault: false, // 类型检查:loading 默认值必须为 boolean},},setup(props) {const loading = ref(props.loading); // 类型检查:props.loading 必须为 boolean // ...return { loading };},template: `<div><Component v-if="!loading" v-bind="$$attrs" /><div v-else>Loading...</div></div>`,});
}