TypeScript 泛型(最后來(lái)一次)
看看这个语法结构。
const 过滤项目 = <T, K extends keyof T>(items: T[], key: K, value: T[K]): T[] => {
// 过滤items数组,返回一个新数组,其中key等于value的所有项目
return items.filter(item => item[key] === value)
}
如果你能轻松理解这种语法,你在这里干嘛呢?出去走走,读读书,走为上策。
但是……
如果你讨厌上面的一切,继续看下去!你将了解所有关于 TS 泛型的知识,以及为什么需要它。我们就会从问“为什么”开始。
PS:如果你更喜欢看视频,我在YouTube上有这个内容的视频版本。
为什么我们需要泛型?
根据定义,“泛型指的是不特定的事物”,但 TypeScript 却侧重于具体性。对于你在 TypeScript 应用中使用的任何数据,编译器都会要求你定义其类型。这就是为什么需要严格的类型检查。但在某些情况下,你在“定义阶段”可能还不清楚具体类型。让我解释一下“定义阶段”。
想象你正在管理一个电子商务网站的后台。每个电子商务应用都会有用户和商品。所以在这个管理控制台上,你会看到所有用户列表和所有产品列表。它们各自的界面看起来会像这样。
interface 用户接口 {
id: number;
名称: string;
email: string;
}
interface 产品接口 {
id: number;
商品标题: string;
price: number;
}
要获取仪表板中的用户和产品列表如下所示,你通常会这样写函数。
const 获取用户 = async (): Promise<User[]> => {
const 响应 = await fetch('<https://api.example.com/users>');
const 数据 = await 响应.json();
return 数据;
};
const 获取产品 = async (): Promise<Product[]> => {
const 响应 = await fetch('<https://api.example.com/products>');
const 数据 = await 响應.json();
return 数据;
};
但这两个函数之间唯一的区别就是API接口。获取内容的内部逻辑几乎是一样的。所以我们来创建一个通用函数。
定义了一个异步函数 fetchData
,用于从给定的 URL 获取数据。
const fetchData = async (url): Promise<any> => {
const response = await fetch(url);
const data = await response.json();
return data;
};
定义了一个异步函数 fetchUsers
,用于从 <https://api.example.com/user/1>
获取用户数据。
const fetchUsers = async () => {
return await fetchData('<https://api.example.com/user/1>');
};
定义了一个异步函数 fetchProducts
,用于从 <https://api.example.com/products>
获取产品数据。
const fetchProducts = async () => {
return await fetchData('<https://api.example.com/products>');
};
我们解决了重复的问题,但是现在有了一个新的问题。我们无法真正知道从API得到的数据类型。由于这是一个通用函数且将用于所有的请求,这意味着我们无法预测将要获取的数据类型。如果你用TS有一段时间,就会发现使用any
和直接使用JavaScript的效果差不多。理想情况下,你的TypeScript代码库中不应该有任何any
关键字,因为它几乎没有类型安全。
那么,你如何解决这个问题呢?我们如何让这个函数既安全又保持通用性呢?通过使用它,那就是所谓的“泛型”。
这个想法在纸上看起来很简单。你给这个函数一个类型占位符参数。让我解释一下。当我们创建这个函数时,我们无法确定响应的类型,因为这适用于所有fetch API调用。但是当我们实际调用这个函数时,我们就知道了它的类型。
所以在仪表板中,当你打开一个显示用户列表的页面时,你知道需要调用哪个API,你也知道服务器会返回什么样的数据。这一点很重要。
在定义某件事物时,你可以让它变得通用,但在使用它时,你可以进一步细化并确保更高的类型安全性。
所以我们调整一下例子。理解泛型现在已经变得简单了。这是一个简单的概念,但语法非常让人头疼。只能硬着头皮去克服。多做练习,最终这些复杂的语法会开始变得有条理。
因此,为了使这个函数具有带有泛型的类型安全,使用尖括号来指定类型占位符。
/**
* 这是一个异步函数,用于从指定URL获取数据并解析响应。
*/
const fetchData = async <T>(url: string): Promise<any> => {
const response = await fetch(url);
const data = await response.json();
return data;
};
通常你会看到人们使用这种语法,其中T
是一个占位符,但你并没有这方面的限制,可以自由命名。你可以用任何名字,它依然有效。我建议你在刚开始时,给它起更有意义的名字,而不是仅仅用字母。所以我们改成 <ResType>
由于此功能在一个jsx文件内,编译器会将其视为一个React组件。因此,再次出现一个不太美观的变通方法来解决这个问题。您可以在<ResType ,> 在类型后面添加一个逗号来解决这个问题。我知道这设计不怎么好,但是时间久了你就会习惯了。
我们现在定义了这个占位符类型,因此我们可以在函数中的任何地方使用它。在这种情况下,它将作为此函数的返回类型,因此我将在 Promise
中放入泛型。
const fetchData = async <T>(url: string): Promise<T> => {
const response = await fetch(url);
// 从给定的URL获取数据并返回
const data = await response.json();
return data;
}
就这样。你的函数现在通过使用泛型已经变得类型安全。在我调用这个函数时,需要为占位符指定实际类型。在 fetchUsers
和 fetchProducts
时,我将这样做:
fetchUsers<String>();
fetchProducts<Integer>();
const fetchUsers = async () => {
return await fetchData<User[]>('<https://api.example.com/user/1>');
};
const fetchProducts = async () => {
return await fetchData<Product[]>('<https://api.example.com/products>');
};
注意!在这个例子中,其实你并不需要泛型。你可以移除泛型参数,将fetchData
函数的返回类型设为any
,并在两个函数内部传递具体的返回类型。就像这样:
const fetchData = async (url: string): Promise<any> => {
const response = await fetch(url);
const data = await response.json();
return data;
};
const fetchUsers = async (): Promise<User[]> => {
// 获取用户数据
return await fetchData('<https://api.example.com/user/1>');
};
const fetchProducts = async (): Promise<Product[]> => {
// 获取产品数据
return await fetchData('<https://api.example.com/products>');
};
所以在这个特定的例子中,你可以不使用泛型仍然获得类似的类型安全性保障。但接下来的例子就是事情开始变得有趣起来。想象一下,你有一个不同类别的产品组成的数组。
const products = [
{ id: 1, name: '笔记本电脑', category: '电子产品' },
{ id: 2, name: '椅子', category: '家具' },
{ id: 3, name: '桌子', category: '家具' },
{ id: 4, name: '手机', category: '电子产品' },
{ id: 5, name: '衬衫', category: '衣服' },
{ id: 6, name: '裤子', category: '衣服' },
];
你想要创建一个类型安全的过滤函数,它接受3个参数:产品数组,键,值。
// 定义一个函数,用于过滤项,基于给定的键和值
const filterItems = (items: any[], key: string, value: any): any[] => {}
所以例如,如果我想只筛选出电子商品,我会用这个函数,将key
设置为类别,将value
设置为电子。这样就能得到所有的电子商品。我也可以根据名称或 ID 来筛选。实际的筛选操作非常简单。
const filterItems = (items: any[], key: string, value: any): any[] => {
// 过滤items数组,根据给定的key和value筛选元素
return items.filter(item => item[key] === value);
};
const filteredProducts = filterItems(products, 'category', 'Furniture');
// 输出过滤后的商品
console.log(filteredProducts);
现在,这是一个通用的函数。所以我可以使用这个筛选函数对几乎所有对象的数组进行筛选。比如,如果我有一个用户数组如下所示,
const users = [
{ id: 1, name: 'John', gender: '男' },
{ id: 2, name: 'James', gender: '男' },
{ id: 3, name: 'Jill', gender: '女' },
{ id: 4, name: 'Joanna', gender: '女' },
{ id: 5, name: 'Jesse', gender: '男' },
{ id: 6, name: 'Jessica', gender: '女' },
];
// 用户列表,包含用户的ID、名字和性别
// 用户列表: id, name, gender
我可以对这一系列用户使用相同的函数和过滤器。这就是为什么它被称为泛型函数。它可以处理任何类型的数据。但是目前它还不支持类型安全性。所以我们来解决这个问题,添加一个类型占位符,即一个泛型。我将它命名为ItemType
,既作为数组类型也作为返回类型。因为数组过滤器返回相同类型的数组,所以我使用相同的泛型作为该函数的返回类型。
const filterItems = <ItemType, >(items: ItemType[], key: string, value: any): ItemType[] => {
return items.filter((item) => item[键] === 值);
};
这个参数虽然有点通用,但我将它作为 string
传递。在这个例子中,它的值只能是 id、name 或 category,对吧?我不希望使用这个函数的人传递除这些键之外的其他任何值。所以我们需要添加一些限制。其实很简单,你只需将 string
改为 keyof Itemtype
即可。
const filterItems = <ItemType, >(items: ItemType[], key: keyof ItemType, value: any): ItemType[] => {
return items.filter((item) => item[key] === value);
};
// 过滤项函数,根据指定的键和值筛选出数组中的元素
这意味着你只能使用传递给函数的对象中存在的键。所以如果我将其改回 string
,我可以传递任何东西作为第二个参数,但一旦我将其设置为 keyof ItemType
,你会发现 TS 会抛出一个错误,说我不能随便传递字符串作为第二个参数。它应该从这些字面量值(id、name 或 category)中选取。
好的,所以我们对第二个参数的类型进行了约束。现在你可能会以为我也会对第三个参数做同样的事情,对吧?我的意思是,当然我也会这么做,但是你还需要考虑一些除了类型安全性之外的问题。这对开发者来说友好吗?我可以有一个除了 any
之外的泛型类型吗?是否真的需要这么严格的类型安全性?在使用泛型前你应该提出这些问题,因为可能有其他更易读且功能相似的方法。
但我还是会去做这件事。
首先,不要将任何类型设为any
。如果我事先知道对象中的每个键可能存在的值的类型,那么我可以将类型设置为联合类型(union)。例如,这个例子中,键的值可以是string
或number
,所以我可以将类型设为string | number
。
但是很明显,这不是正确的方法,因为一个对象可以有很多键,每个键的类型也可能不同。你得一个个手动找到每个类型的键并设置值。所以这不行。我们可以引入另一种占位符类型来表示值。没错,你可以同时创建多种泛型类型!
这里我将介绍一种新的类型,称之为KeyType
,现在值的类型变为了ItemType[KeyType]
const filterItems = <ItemType, KeyType>(items: ItemType[], key: keyof ItemType, value: ItemType[KeyType]): ItemType[] => {
// 定义了一个过滤函数,用于根据给定的键和值来筛选项目数组
return items.filter((item) => item[key] === value);
};
就像用于对象的方括号表示法一样,我们也可以使用类似的方法。ItemType
指的是对象本身,KeyType
则是指这个对象里的任意键,比如 id、name 或 category 等。
因为我现在为键创建了一种类型,我可以将我原本放在函数中的约束条件直接移入泛型定义,并可以直接把 Keytype
用作第二个参数的类型。
const filterItems = <ItemType, KeyType keyof ItemType>(items: ItemType[], key: KeyType, value: ItemType[KeyType]): ItemType[] => {
return items.filter((item) => item[key] === value);
}
但是,对于泛型来说,如果你想对类型进行限制,你需要使用 extends
关键字。所以我将在 keyof
前面加上 extends
关键字。
// 过滤项目:根据项的某个键值筛选项目列表
const filterItems = <ItemType, KeyType extends keyof ItemType>(items: ItemType[], key: KeyType, value: ItemType[KeyType]): ItemType[] => {}
所以再次,我们有 ItemType
,它将会作为数组内对象的类型。第二个类型是 KeyType
。这个 KeyType
类型只能是 ItemType
的键。因此在这种情况下,KeyType
只能是 id、name 和 category,这正是我们希望第二个参数所表现的。第三个参数的值被设置为 ItemType[KeyType]
,这意味着无论你传递给第二个参数的是什么键,TS 会根据你传递的第二个参数(例如 category)查看对象内所有可能的值类型。对于我们来说,应该支持的值有 Electronics、Clothing 和 Furniture。
但我在 IntelliSense 中没有任何提示(你可以试一下。)。这是因为 TS 推断 category 的类型是字符串。但我们知道 category 会有一组特定的字面量值。因此我可以在这里做一件事。我可以告诉 TS 这是一个字面量值,而不是随机字符串。要做到这一点,我可以将其设置为“常量值”。
const products = [
{ id: 1, name: '笔记本电脑', category: '电子设备' },
{ id: 2, name: '椅子', category: '家具' },
{ id: 3, name: '桌子', category: '家具' },
{ id: 4, name: '手机', category: '电子设备' },
{ id: 5, name: '衬衫', category: '服装' },
{ id: 6, name: '裤子', category: '服装' },
];
现在你应该会看到一个错误,该错误指出了基于第二个参数的键,第三个参数期望的类型错误。虽然 TS 已经为 name 和 ID 推断出了泛型类型,我会将整个内容转换为字面量来解决这些问题。
const products = [
{ id: 1, name: '笔记本', category: '电子产品' },
{ id: 2, name: '椅子', category: '家具' },
{ id: 3, name: '桌子', category: '家具' },
{ id: 4, name: '智能手机', category: '电子产品' },
{ id: 5, name: 'T恤', category: '服装' },
{ id: 6, name: '裤子', category: '服装' },
];
现在,对象里的每个键对应的值都会是字面量类型。
但…
(或者但,也合适)
实际上,这种情况是不会发生的。如果你有一个包含一百万个项目 的列表,TS为一百万个名称计算字面值会非常耗资源。同样的,对于ID来说也是一样。而类别只是一些事先已知的有限值。所以你可以简单地为产品创建一个类型,并在列表中使用它。
type Product = {
id: number;
name: string;
category: '电子产品' | '服装' | '家具'
}
const products : Product[] = [
{ id: 1, name: 'Laptop', category: '电子产品' },
{ id: 2, name: 'Chair', category: '家具' },
{ id: 3, name: 'Desk', category: '家具' },
{ id: 4, name: 'Mobile Phone', category: '电子产品' },
{ id: 5, name: 'Shirts', category: '服装' },
{ id: 6, name: 'Pants', category: '服装' },
];
现在我不再需要在数组里的每个对象上都加上 const 关键字。如果对于一个新的键,我已经知道它只会包含几个具体的值,而不会是一个随意的字符串,我就可以像定义 category 一样,为它定义一个联合类型。这样仍然可以保证类型安全。如果我使用 category 作为第二个参数,而第三个参数是任意值,TS 就会报错。
这是如何使用泛型来创建可以在不同数据类型上使用而不会失去类型安全性的函数。通常你会看到,例如我会用一堆“T”
来代替我定义的类型,如下所示,
const filterItems = <T, K extends keyof T>(items: T[], key: K, value: T[K]): T[] => {
return items.filter((item) => item[key] === value);
};
// 这个函数用于过滤列表,根据给定的键和值来筛选出符合条件的项。
我们又回到了起点。
而且,我完全忘记了这一点,但我确定你已经用过泛型了。你可能自己没写过泛型,但以 filter
函数为例。你看看它的 TS 定义,你会发现同样的 T
。
让我再给你举一个例子,这次是在一个 React 应用里。你肯定用过 useState
钩子吧,这样更符合口语习惯。如果你查看它的定义,你还会看到一个泛型,使句子更加流畅。这是因为 useState
无法预知我们传递给它的值是什么,更准确地传达原意。我们可以传递任何我们想传递的东西,使句子更自然。
我的意思是,泛型无处不在,几乎不可能你没用过它们。
我也觉得语法看起来有点丑,我也同意你有一些替代方法,但是如果你真的理解了它,你可以用更通用且类型安全的方式来减少很多重复的代码。请阅读相关文档,试着自己创建一些泛型函数、类型、组件和接口。一旦你掌握了,你就会发现离不开它了。
我在Youtube上也有这个视频讲解,如果你更喜欢通过视频来理解。(链接)
除此之外,如果还有任何疑问或建议,欢迎在评论区留言或通过我的任何社交媒体联系我。祝好运!
YouTube (YouTube频道) LinkedIn (领英个人主页) Twitter (推特账号) GitHub (GitHub主页)
简单英语 🚀感谢您加入In Plain English社区!在您离开前,
- 别忘了鼓掌并关注作者 ️👏️️
- 关注我们: X | LinkedIn | YouTube | Discord | 通讯简报 | 播客(Podcast)
- 在Differ上免费创建一个AI驱动的博客。
- 更多精彩内容,请访问 PlainEnglish.io
共同學(xué)習(xí),寫下你的評(píng)論
評(píng)論加載中...
作者其他優(yōu)質(zhì)文章