在编程世界里,我们经常会遇到一个情况:阅读那些充满了虚构示例的枯燥文档,实在是让人提不起兴趣。因此,在这篇文章中,我想和大家分享一些我在实际开发过程中遇到的泛型(Generics)使用案例。通过这些真实的例子,相信泛型的概念对你来说会更加具有意义,也更容易理解。
泛型简介
那么,泛型究竟是什么呢?简而言之,泛型允许我们编写能够适用于广泛的原始类型和对象的类型安全代码。在声明新类型、接口、函数和类时,都可以使用泛型。这听起来可能有点抽象,那么让我们直接进入正题,看看泛型的一些实际用例吧。
代码重复
有时候,在我们开发的时候会遇到一些重复性的工作,特别是当我们要处理不同类型的数据时。这里有个很好的例子,就是我们的服务器需要返回用户和书籍信息。通常情况下,如果没有泛型(Generics),我们可能需要为每种资源分别定义一个响应类型。
举个例子,你的服务器需要返回用户信息和书籍信息。
如果没有泛型,你可能需要为用户和书籍分别定义两个相似的响应类型,就像下面这样:
// 用户信息类型
type User = { name: string };
type UsersResponse = {
data: User[];
total: number;
page: number;
limit: number;
};
// 书籍信息类型
type Book = { isbn: string };
type BooksResponse = {
data: Book[];
total: number;
page: number;
limit: number;
};
但这种方式会产生很多重复的代码,不利于后期维护。而泛型,它的妙处就在于可以让我们定义一个通用的响应形状,然后再根据需要使用不同的数据类型来复用这个形状,这样就能减少重复的代码,看看下面这个改进版:
// 分页响应的泛型定义
type PaginatedResponse<T> = {
data: T[];
total: number;
page: number;
limit: number;
};
// 使用泛型定义用户和书籍的响应类型
type UsersResponse = PaginatedResponse<User>;
type BooksResponse = PaginatedResponse<Book>;
使用了泛型之后,无论是处理用户列表还是书籍列表,我们只需要写一次响应结构,就可以应用到各种不同的数据类型上了,不是很方便吗?
泛型就像是一个万能的模具,你只需要根据不同的需求,换上不同的"原料",它就能帮你塑形出符合要求的"产品"。这样我们的代码就会变得更简洁、更有可读性,也更容易维护。
现在来想想,你是否能在你的项目中找到那些可以用泛型来简化的地方呢?别小看这个小改变,它可能会为你省下不少时间和精力哦!
泛型,让函数的逻辑和类型更匹配
在软件开发中,我们常常需要编写一些根据特定属性筛选数组元素的函数。比如我们有一个筛选数组的函数 filterArrayByValue,它可以基于我们提供的属性和值来过滤数组。函数的参数和返回值之间的关系非常紧密。
一开始,我们的函数可能看起来是这样的:
function filterArrayByValue(items, propertyName, valueToFilter) {
return items.filter((item) => item[propertyName] === valueToFilter);
}
这个函数声明说,它接受一个项目数组,并返回一个具有相同类型项目的数组。目前为止,一切都好。
但是这里有个问题,我们的 propertyName 参数被定义为字符串类型,这看似没问题,但它可能会导致我们不小心传入了不存在于类型 T 的项的属性名。如果我们定义了一个用户数组,它应该是这样的:
type User = { name: string; age: number };
const users: User[] = [
{ name: 'Vasya', age: 32 },
{ name: 'Anna', age: 12 },
];
现在,如果我们尝试传递一个错误的属性,在这种情况下它不会破坏应用程序,只是返回一个空数组,但是这并不是我们希望的,我们希望编译器会提示属性不匹配的问题。
filterArrayByValue(users, 'notExistField', 'Vasya');
让我们定义该函数的第二个参数,它将描述限定为只能为T类型的相关的属性
我们定义完后,发现在运行阶段之前提示传递了错误的属性。
接下来我们使用 number 类型的age 属性。正如您可能预测的那样,当我们尝试按此字段过滤项目时,我们会遇到问题:
filterArrayByValue(users, 'age', 12);
接下来我们修改过滤函数,valueToFilter参数的对应关系,匹配为T类型属性对应的值
修改后,问题已经消失了,现在我们无法将除了数字以外的其他类型的值作为年龄属性值传递,因为用户类型只允许该属性为数字,这正是我们需要的。
在 React 中的应用
在React开发中,状态管理是一个核心概念,尤其是在使用函数组件和Hooks的时候。给出的代码段展示了如何在React组件中使用 useState Hook来管理一个用户对象的状态,并提供了一个 setUserField 函数来更新用户对象的特定字段。原始版本的函数对于字段名和字段值使用了非常宽泛的类型定义,这可能会导致类型安全问题。
const setUserField = (field: string, value: any) =>
setUser(prevUser => ({...prevUser, [field]: value}));
这里,field 是任意的字符串,而 value 是任意类型,这意味着我们可以不小心将错误的数据类型赋值给用户对象的属性,TypeScript编译器也不会提出警告。
为了提高类型安全性,可以使用泛型来约束 field 必须是 User 类型的键,value 必须是对应于该键的 User 类型的值。改进后的 setUserField 函数如下:
function setUserField<KEY extends keyof User>(
field: KEY,
value: User[KEY]
) {
setUser((prevUser) => ({ ...prevUser, [field]: value }));
}
在这个改进的版本中,setUserField 函数现在接受两个参数:
- field:一个类型参数 KEY,它被限制为 User 类型的键的集合中的一个。
- value:一个 User[KEY] 类型的值,确保了传递给 setUserField 的值必须与 User 类型中 field 字段的类型相匹配。
这样一来,如果你尝试传递一个不正确的字段或者错误类型的值给 setUserField 函数,TypeScript编译器会提供类型错误的提示,从而减少运行时错误的可能性。这种模式特别有用,因为它可以保证我们对状态的更新是类型安全的,同时也保持了函数的灵活性。这是React中使用TypeScript的一个典型例子,展示了如何通过类型系统来增强代码质量。
同时保持灵活和严格(关键词“扩展extend”与泛型)
当我们在设计高阶组件(HOC)时,尤其是在React或React Native的环境下,我们希望这些HOC只能应用于具有某些属性的组件。在这个例子中,我们想要一个HOC,它仅适用于具有 style 属性的组件。
function withStyledComponent<StyleProp, Props extends { style?: StyleProp }>(
Component: ComponentType<Props>
) {
return (props: Props) => {
const { style } = props;
// 实现细节在此省略
return <Component {...props} />;
};
}
泛型的 extend 关键字允许我们定义一个类型 T,它必须至少具有类型 K 的所有属性。这样,我们就可以确保我们的HOC只会被用在正确的组件上。
在上述的 withStyledComponent HOC中,我们指定了任何使用此HOC的组件都必须有一个 style 属性。如果我们尝试将这个HOC应用于没有 style 属性的组件,TypeScript会抛出一个错误。
这种模式非常有用,因为它可以保证我们的HOC在类型安全的同时,也不限制组件的其他属性。这就意味着,尽管我们对 style 属性有明确的期望,但我们的组件可以自由地具有其他任何属性。
此外,由于TypeScript知道我们可能会在具有 style 属性的组件中使用我们的HOC,我们可以安全地从组件的属性中提取 style 并在HOC内部操作它。
TypeScript中的类型推断
TypeScript有一个令人惊叹的特性——它会尝试从上下文中推断出类型,只要有可能。比如,在代码中看到这样的语句时:
const a: number = 12;
这意味着开发者可能并不知道TypeScript已经知道a是一个从值推断出来的数字类型。
现在,假设我们用泛型定义了这样一个函数:
function identifyType<T>(target: T) {
console.log("Type of target is", typeof target);
}
如果你是初学者,你可能会这样使用它:
identifyType<number>(5);
但是,TypeScript可以从你作为第一个参数传递的值中推断出泛型的类型,最好是这样使用:
identifyType(5);
如果你是React开发者,你可能会经常看到像这样的代码片段:
const [count, setCount] = useState<number>(5);
但同样,这里明确定义泛型类型是多余的,因为它会从你作为第一个参数传递的值中被推断出来。如果你是一位经验丰富的开发者,你的代码将看起来像这样:
const [count, setCount] = useState(5);
还有我遇到过的一个情况,有开发者害怕在React组件的props中使用泛型。是的,我们在JSX中使用我们的组件,他们不知道这样的语法是有效的:
function Component() {
const data: ItemType[] = [{ value: '1' }];
return (
<RenderList<ItemType>
data={data}
renderItem={({ item }) => <Text>{item.value}</Text>}
/>
);
}
是的,它看起来有些奇怪,但这里我们可以依靠TypeScript的能力,根据我们传递给组件的props类型来推断泛型类型:
<RenderList
data={data}
renderItem={({ item }) => <Text>{item.value}</Text>}
/>
认同这样的代码看起来更简洁,你看起来也像一个经验丰富的开发者。这就是TypeScript和泛型的魅力:它们提供了一种强大的类型系统,不仅可以帮助我们减少错误,还可以使代码更加简洁易读。通过这些例子,我们可以看到,TypeScript的类型推断功能可以在不牺牲类型安全的情况下,极大地简化代码。而泛型的灵活使用,则让我们的代码既严谨又富有弹性。
结束
在我们今天的旅程中,我们一起探索了TypeScript中那些令人兴奋的泛型知识。从类型推断的便捷性到泛型在日常编程中的灵活运用,希望这些内容能够帮助你解开围绕泛型的所有迷雾。记住,泛型不仅仅是类型安全的保障,它还能让你的代码更加简洁、更易于维护。
正如我们所见,合理利用TypeScript的类型推断,可以让我们避免冗余的代码,让逻辑表达更为直观。泛型的使用更是让组件和函数的复用性达到了新的高度。所以,当你下次遇到需要类型化处理多样化数据的场景时,别忘了,泛型就是你的得力助手。