前言
大家好,我是小杨。还记得我刚学习TypeScript时,最让我头疼的就是泛型这个概念。什么T、U、K,看起来像密码一样神秘。但当我真正理解并开始使用泛型后,才发现它就像是TypeScript中的"瑞士军刀",能让我们的代码既灵活又类型安全。今天,我想和大家分享我对于TypeScript泛型的理解和实战经验。
什么是泛型?从函数参数到类型参数
想象一下,如果你要写一个函数,它既能处理数字,又能处理字符串,还能处理任何其他类型,你会怎么做?
在JavaScript中,我们可能会这样写:
typescript
// JavaScript方式 - 缺乏类型安全
function identity(value) {
return value;
}
const num = identity(42); // 返回42,但类型信息丢失了
const str = identity("hello"); // 返回"hello",类型信息丢失了
而在TypeScript中,泛型给了我们更好的解决方案:
typescript
csharp
// TypeScript泛型 - 保持类型安全
function identity<T>(value: T): T {
return value;
}
const num = identity(42); // 类型为 number
const str = identity("hello"); // 类型为 string
const bool = identity(true); // 类型为 boolean
这里的<T>就是泛型参数,它像一个"类型变量",在函数被调用时确定具体的类型。
泛型基础:从简单到复杂
1. 泛型函数
让我们从一个实际的例子开始:
typescript
// 一个简单的栈实现
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
size(): number {
return this.items.length;
}
}
// 使用示例 - 类型安全!
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
// numberStack.push("hello"); // ❌ 编译错误:不能将字符串压入数字栈
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");
2. 多个泛型参数
typescript
// 处理键值对的函数
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
// 使用示例
const stringNumberPair = pair("age", 25); // [string, number]
const numberBooleanPair = pair(1, true); // [number, boolean]
const complexPair = pair("config", { debug: true }); // [string, { debug: boolean }]
3. 泛型约束
有时候,我们需要对泛型参数做一些限制:
typescript
// 要求泛型参数必须有length属性
interface HasLength {
length: number;
}
function getLength<T extends HasLength>(item: T): number {
return item.length;
}
// 使用示例
getLength("hello"); // ✅ 字符串有length
getLength([1, 2, 3]); // ✅ 数组有length
getLength({ length: 5 }); // ✅ 对象有length属性
// getLength(42); // ❌ 数字没有length属性
实战场景:泛型在项目中的应用
场景1:API响应处理
在我的实际项目中,泛型在API层发挥了巨大作用:
typescript
// 定义通用的API响应类型
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
timestamp: number;
}
// 通用的API请求函数
async function apiRequest<T>(
endpoint: string,
options?: RequestInit
): Promise<ApiResponse<T>> {
const response = await fetch(`/api/${endpoint}`, options);
const result: ApiResponse<T> = await response.json();
return result;
}
// 定义具体的数据类型
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
category: string;
}
// 使用示例 - 完美的类型安全!
const userResponse = await apiRequest<User>("users/1");
console.log(userResponse.data.name); // ✅ 正确的属性访问
// console.log(userResponse.data.invalid); // ❌ 编译错误
const productResponse = await apiRequest<Product>("products/123");
console.log(productResponse.data.price); // ✅ 正确的属性访问
场景2:工具函数库
泛型让工具函数变得更加通用和类型安全:
typescript
// 数组工具函数
function filterArray<T>(
array: T[],
predicate: (item: T, index: number) => boolean
): T[] {
return array.filter(predicate);
}
function mapArray<T, U>(
array: T[],
mapper: (item: T, index: number) => U
): U[] {
return array.map(mapper);
}
// 对象工具函数
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
function mergeObjects<T extends object, U extends object>(
obj1: T,
obj2: U
): T & U {
return { ...obj1, ...obj2 };
}
// 使用示例
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = filterArray(numbers, n => n % 2 === 0); // number[]
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
];
const userNames = mapArray(users, user => user.name); // string[]
const person = { name: "Alice", age: 30 };
const name = getProperty(person, "name"); // string
// const invalid = getProperty(person, "email"); // ❌ 编译错误
场景3:状态管理
在React项目中,泛型可以帮助我们创建类型安全的Hook:
typescript
import { useState, useCallback } from 'react';
// 通用的表单Hook
function useForm<T extends Record<string, any>>(initialValues: T) {
const [values, setValues] = useState<T>(initialValues);
const setValue = useCallback(<K extends keyof T>(key: K, value: T[K]) => {
setValues(prev => ({ ...prev, [key]: value }));
}, []);
const reset = useCallback(() => {
setValues(initialValues);
}, [initialValues]);
return {
values,
setValue,
reset,
setValues
};
}
// 使用示例
interface LoginForm {
email: string;
password: string;
rememberMe: boolean;
}
function LoginComponent() {
const { values, setValue } = useForm<LoginForm>({
email: "",
password: "",
rememberMe: false
});
// 完全类型安全!
const handleEmailChange = (email: string) => {
setValue("email", email); // ✅ 正确
};
const handleRememberChange = (remember: boolean) => {
setValue("rememberMe", remember); // ✅ 正确
};
// setValue("invalidKey", "value"); // ❌ 编译错误
// setValue("email", 123); // ❌ 编译错误
}
场景4:高阶组件和渲染Props
typescript
// 带加载状态的高阶组件
function withLoading<TProps extends object>(
Component: React.ComponentType<TProps>
) {
return function WithLoadingComponent(props: TProps & { isLoading?: boolean }) {
const { isLoading, ...componentProps } = props;
if (isLoading) {
return <div>Loading...</div>;
}
return <Component {...componentProps as TProps} />;
};
}
// 数据获取组件
interface DataRendererProps<T> {
url: string;
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode;
}
function DataRenderer<T>({ url, children }: DataRendererProps<T>) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then((data: T) => {
setData(data);
setLoading(false);
})
.catch((err: Error) => {
setError(err);
setLoading(false);
});
}, [url]);
return <>{children(data, loading, error)}</>;
}
// 使用示例
interface UserData {
id: number;
name: string;
email: string;
}
function UserProfile() {
return (
<DataRenderer<UserData> url="/api/user/1">
{(user, loading, error) => {
if (loading) return <div>Loading user...</div>;
if (error) return <div>Error: {error.message}</div>;
if (user) return <div>Hello, {user.name}!</div>;
return null;
}}
</DataRenderer>
);
}
高级泛型技巧
1. 条件类型
typescript
// 根据条件选择类型
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
// 提取数组元素类型
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type Numbers = ArrayElement<number[]>; // number
type Strings = ArrayElement<string[]>; // string
type Mixed = ArrayElement<(number | string)[]>; // number | string
2. 映射类型
typescript
// 让所有属性变为可选
type Partial<T> = {
[P in keyof T]?: T[P];
};
// 让所有属性变为只读
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 实际应用
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>;
// 等价于 { id?: number; name?: string; email?: string; }
type ReadonlyUser = Readonly<User>;
// 等价于 { readonly id: number; readonly name: string; readonly email: string; }
3. 泛型工具类型实战
typescript
// 创建请求参数类型
interface ApiEndpoints {
users: {
GET: { id: number };
POST: { name: string; email: string };
};
products: {
GET: { category?: string };
POST: { title: string; price: number };
};
}
// 自动生成请求参数类型
type RequestParams<TEndpoint extends keyof ApiEndpoints, TMethod extends keyof ApiEndpoints[TEndpoint]>
= ApiEndpoints[TEndpoint][TMethod];
// 使用示例
type GetUserParams = RequestParams<"users", "GET">; // { id: number }
type CreateUserParams = RequestParams<"users", "POST">; // { name: string; email: string }
type GetProductParams = RequestParams<"products", "GET">; // { category?: string }
常见陷阱和最佳实践
1. 不要过度使用泛型
typescript
// 不推荐:过度复杂的泛型
function overlyComplex<T extends Record<string, any>, K extends keyof T, U extends T[K]>(
obj: T,
key: K,
transformer: (value: T[K]) => U
): U {
return transformer(obj[key]);
}
// 推荐:保持简单
function getAndTransform<T, U>(
obj: Record<string, T>,
key: string,
transformer: (value: T) => U
): U {
return transformer(obj[key]);
}
2. 提供合理的默认值
typescript
// 为泛型参数提供默认值
interface PaginationOptions<T = any> {
page: number;
pageSize: number;
filter?: (item: T) => boolean;
sort?: (a: T, b: T) => number;
}
// 使用默认值
const defaultOptions: PaginationOptions = {
page: 1,
pageSize: 10
};
// 指定具体类型
const userOptions: PaginationOptions<User> = {
page: 1,
pageSize: 20,
filter: user => user.active,
sort: (a, b) => a.name.localeCompare(b.name)
};
3. 合理使用类型推断
typescript
// 让TypeScript自动推断类型
function createArray<T>(...items: T[]): T[] {
return items;
}
// 自动推断为number[]
const numbers = createArray(1, 2, 3);
// 自动推断为string[]
const strings = createArray("a", "b", "c");
// 自动推断为(string | number)[]
const mixed = createArray(1, "two", 3);
结语
泛型是TypeScript中最强大的特性之一,它让我们的代码在保持类型安全的同时,获得了极大的灵活性。从简单的工具函数到复杂的系统架构,泛型都能发挥重要作用。
记住学习泛型的关键:
- 从简单的用例开始,逐步深入
- 多实践,在真实项目中应用
- 不要害怕犯错,TypeScript编译器会指导你
泛型就像编程中的"魔法",一旦掌握,你就会发现它能解决很多之前觉得棘手的问题。希望今天的分享能帮助你在TypeScript的道路上更进一步!
⭐ 写在最后
请大家不吝赐教,在下方评论或者私信我,十分感谢🙏🙏🙏.
✅ 认为我某个部分的设计过于繁琐,有更加简单或者更高逼格的封装方式
✅ 认为我部分代码过于老旧,可以提供新的API或最新语法
✅ 对于文章中部分内容不理解
✅ 解答我文章中一些疑问
✅ 认为某些交互,功能需要优化,发现BUG
✅ 想要添加新功能,对于整体的设计,外观有更好的建议
✅ 一起探讨技术加qq交流群:906392632
最后感谢各位的耐心观看,既然都到这了,点个 👍赞再走吧!