React 和 TypeScript:使用泛型改进你的类型
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
TypeScript 对 React 开发者来说简直是福音,但它的语法对新手来说却相当吓人。我认为泛型是造成这种现象的主要原因:它们看起来很奇怪,用途也不明显,而且解析起来相当困难。
本文旨在帮助你理解和揭开 TypeScript 泛型的神秘面纱,并重点介绍它们在 React 中的应用。泛型其实并不复杂:如果你理解函数,那么理解泛型也就不难了。
TypeScript中的泛型是什么?
为了理解泛型,我们首先将标准的 TypeScript 类型与 JavaScript 对象进行比较。
// a JavaScript object
const user = {
name: 'John',
status: 'online',
};
// and its TypeScript type
type User = {
name: string;
status: string;
};
如你所见,两者非常接近。主要区别在于,JavaScript 关注的是变量的值,而 TypeScript 关注的是变量的类型。
我们可以看出,我们定义的User类型存在一个问题,那就是它的status属性过于模糊。状态通常具有预定义的值,例如,在本例中,它可以是“在线”或“离线”。我们可以修改我们的类型:
type User = {
name: string;
status: 'online' | 'offline';
};
但这假设我们已经知道有哪些状态类型。如果我们不知道,而且实际的状态列表发生了变化怎么办?这就是泛型的作用所在:它们允许你指定一个可以根据使用情况而变化的类型。
稍后我们将探讨如何实现这种新类型,但对于我们的User示例而言,使用泛型类型如下所示:
// `User` is now a generic type
const user: User<'online' | 'offline'>;
// we can easily add a new status "idle" if we want
const user: User<'online' | 'offline' | 'idle'>;
上面的意思是“该user变量是 类型的对象User,顺便说一下,该用户的状态选项为‘在线’或‘离线’”(在第二个示例中,您将‘空闲’添加到该列表中)。
好吧,带尖括号的语法< >看起来确实有点怪。我同意。不过习惯就好。
是不是很酷?现在我们来看看如何实现这种类型:
// generic type definition
type User<StatusOptions> = {
name: string;
status: StatusOptions;
};
StatusOptions它被称为“类型变量”,并且User被认为是“泛型类型”。
你可能觉得它看起来很奇怪。但这实际上只是一个函数!如果我用类似 JavaScript 的语法(不是有效的 TypeScript 语法)来编写它,它看起来会像这样:
type User = (StatusOption) => {
return {
name: string;
status: StatusOptions;
}
}
如你所见,它实际上就是 TypeScript 中函数的等效物。而且你可以用它做很多很酷的事情。
例如,假设我们User接受的不是像以前那样单个状态,而是一个状态数组。使用泛型类型仍然很容易实现这一点:
// defining the type
type User<StatusOptions> = {
name: string;
status: StatusOptions[];
};
// the type usage is still the same
const user: User<'online' | 'offline'>;
如果你想了解更多关于泛型的知识,可以查看TypeScript 的相关指南。
为什么泛型非常有用
既然你已经了解了泛型类型及其工作原理,你可能会问自己为什么需要它。毕竟,上面的例子相当人为:你可以定义一个类型Status并使用它:
type Status = 'online' | 'offline';
type User = {
name: string;
status: Status;
};
在这个(相当简单的)例子中确实如此,但在很多情况下,这样做是行不通的。通常情况下,当你想在多个实例中使用同一个类型,而每个实例又有所不同时,就会出现这种情况:你希望这个类型是动态的,能够根据它的使用方式进行调整。
一个非常常见的例子是,函数返回的数据类型与其参数类型相同。最简单的形式是恒等函数,它返回给定的任何类型:
function identity(arg) {
return arg;
}
很简单,对吧?但是如果参数可以是任何类型,该怎么写呢arg?别跟我说用any!
没错,就是仿制药:
function identity<ArgType>(arg: ArgType): ArgType {
return arg;
}
我再次发现这种语法有点复杂,难以理解,但它实际上只是在说:“该identity函数可以接受任何类型(ArgType),并且该类型既是其参数的类型,也是其返回类型的类型”。
以下是如何使用该函数并指定其类型:
const greeting = identity<string>('Hello World!');
在这个特定情况下,<string>由于 TypeScript 可以自行推断类型,所以没有必要这样做,但有时它无法推断(或者推断错误),这时你就必须自己指定类型。
多类型变量
你不局限于使用一种类型的变量,你可以使用任意多种类型。例如:
function identities<ArgType1, ArgType2>(
arg1: ArgType1,
arg2: ArgType2
): [ArgType1, ArgType2] {
return [arg1, arg2];
}
在这种情况下,identities它接受 2 个参数并将它们以数组的形式返回。
JSX 中箭头函数的泛型语法
你可能已经注意到,目前我只使用了常规函数语法,而没有使用 ES6 中引入的箭头函数语法。
// an arrow function
const identity = (arg) => {
return arg;
};
原因在于 TypeScript 对箭头函数的处理不如普通函数(在使用 JSX 时)。你可能会想,你可以这样做:
// this doesn't work
const identity<ArgType> = (arg: ArgType): ArgType => {
return arg;
}
// this doesn't work either
const identity = <ArgType>(arg: ArgType): ArgType => {
return arg;
}
但这在 TypeScript 中行不通。你需要执行以下操作之一:
// use this
const identity = <ArgType,>(arg: ArgType): ArgType => {
return arg;
}
// or this
const identity = <ArgType extends unknown>(arg: ArgType): ArgType => {
return arg;
}
我建议使用第一种方案,因为它更简洁,但我觉得逗号看起来还是有点奇怪。
需要说明的是,这个问题源于我们使用了 TypeScript 和 JSX(简称 TSX)。在普通的 TypeScript 中,你不需要使用这种变通方法。
关于类型变量名称的一点警告
不知何故,在 TypeScript 世界中,给泛型类型的类型变量赋予一个字母的名称是一种惯例。
// instead of this
function identity<ArgType>(arg: ArgType): ArgType {
return arg;
}
// you would usually see this
function identity<T>(arg: T): T {
return arg;
}
使用完整的单词作为类型变量名确实会使代码变得相当冗长,但我仍然认为它比使用单个字母选项更容易理解。
我鼓励你在通用名称中使用实际的单词,就像你在代码其他地方所做的那样。但请注意,在实际应用中,你很可能会经常看到单字母版本。
额外福利:来自开源软件的通用类型示例:useState它本身!
为了总结关于泛型类型的这一部分,我想不妨看看泛型类型的实际应用案例。还有什么比 React 库本身更好的例子呢?
友情提示:本节内容比本文其他部分略微复杂一些。如果您一开始没理解,可以稍后再阅读。
让我们来看一下我们心爱的 hook 的类型定义useState:
function useState<S>(
initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>];
你不能说我没警告过你——使用泛型的类型定义并不美观。或许只有我这么觉得吧!
总之,让我们一步一步地理解这个类型定义:
- 我们首先定义一个函数,
useState它接受一个名为的泛型类型S。 - 该函数接受且仅接受一个参数:一个
initialState。S初始状态可以是类型为(我们的通用类型)的变量,也可以是返回类型为的函数S。
useState然后返回一个包含两个元素的数组:- 第一个是类型
S(这是我们的状态值)。 - 第二个类型是应用了
Dispatch泛型类型的类型SetStateAction<S>。SetStateAction<S>它本身就是应用了SetStateAction泛型类型的类型S(它是我们的状态设置器)。
- 第一个是类型
最后一部分有点复杂,所以我们再深入探讨一下。
首先,我们来查一下SetStateAction:
type SetStateAction<S> = S | ((prevState: S) => S);
好的,所以SetStateAction它也是一个通用类型,它可以是类型为的变量,也可以是参数类型和返回类型都为的S函数。S
这让我想起了我们提供给它的服务setState,对吧?您可以直接提供新的状态值,也可以提供一个函数,该函数根据旧状态值构建新的状态值。
现在是什么Dispatch?
type Dispatch<A> = (value: A) => void;
好的,这个函数只有一个参数,参数类型为泛型类型,并且不返回任何值。
综合起来:
// this type:
type Dispatch<SetStateAction<S>>
// can be refactored into this type:
type (value: S | ((prevState: S) => S)) => void
所以这是一个接受一个值S或一个函数作为参数S => S,但不返回任何值的函数。
这确实符合我们对setState.
这就是 `!` 的完整类型定义。useState实际上,该类型是重载的(这意味着根据上下文,可能适用其他类型定义),但这是主要的。另一个定义只处理不给 `!` 传递任何参数的情况useState,所以initialState是` undefined!`。
供您参考:
function useState<S = undefined>(): [
S | undefined,
Dispatch<SetStateAction<S | undefined>>
];
在 React 中使用泛型
现在我们已经了解了 TypeScript 泛型类型的基本概念,接下来我们可以看看如何在 React 代码中应用它。
React Hooks 的通用类型useState
Hooks 本质上就是普通的 JavaScript 函数,只是 React 对它们的处理方式略有不同。因此,在 Hook 中使用泛型类型与在普通 JavaScript 函数中使用泛型类型是相同的:
// normal JavaScript function
const greeting = identity<string>('Hello World');
// useState
const [greeting, setGreeting] = useState<string>('Hello World');
在上面的示例中,您可以省略显式的泛型类型,因为 TypeScript 可以从参数值中推断出来。但有时 TypeScript 无法做到这一点(或者推断错误),这时就需要使用这种语法。
我们将在下一节中看到一个实际的例子。
如果你想学习如何在 React 中定义所有 Hook,敬请期待!下周我们将发布一篇相关文章。订阅即可第一时间阅读!
组件属性的通用类型
假设你要Select为表单构建一个组件,类似这样:
import { useState, ChangeEvent } from 'react';
function Select({ options }) {
const [value, setValue] = useState(options[0]?.value);
function handleChange(event: ChangeEvent<HTMLSelectElement>) {
setValue(event.target.value);
}
return (
<select value={value} onChange={handleChange}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
}
export default Select;
// `Select` usage
const mockOptions = [
{ value: 'banana', label: 'Banana 🍌' },
{ value: 'apple', label: 'Apple 🍎' },
{ value: 'coconut', label: 'Coconut 🥥' },
{ value: 'watermelon', label: 'Watermelon 🍉' },
];
function Form() {
return <Select options={mockOptions} />;
}
event如果您不确定对象类型发生了什么handleChange,我写了一篇文章解释如何在 React 中使用 TypeScript 处理事件。
假设对于value选项,我们可以接受字符串或数字,但不能同时接受两者。您将如何在Select组件中强制执行此操作?
以下方法没有达到我们预期的效果,你知道为什么吗?
type Option = {
value: number | string;
label: string;
};
type SelectProps = {
options: Option[];
};
function Select({ options }: SelectProps) {
const [value, setValue] = useState(options[0]?.value);
function handleChange(event: ChangeEvent<HTMLSelectElement>) {
setValue(event.target.value);
}
return (
<select value={value} onChange={handleChange}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
}
之所以行不通,是因为在一个options数组中,一个选项的值可以是数字类型,而另一个选项的值可以是字符串类型。我们不希望这样,但 TypeScript 却能接受。
// this would work with the previous `Select`
const mockOptions = [
{ value: 123, label: 'Banana 🍌' },
{ value: 'apple', label: 'Apple 🍎' },
{ value: 'coconut', label: 'Coconut 🥥' },
{ value: 'watermelon', label: 'Watermelon 🍉' },
];
强制要求输入数字或整数的方法是使用泛型:
type OptionValue = number | string;
type Option<Type extends OptionValue> = {
value: Type;
label: string;
};
type SelectProps<Type extends OptionValue> = {
options: Option<Type>[];
};
function Select<Type extends OptionValue>({ options }: SelectProps<Type>) {
const [value, setValue] = useState<Type>(options[0]?.value);
function handleChange(event: ChangeEvent<HTMLSelectElement>) {
setValue(event.target.value);
}
return (
<select value={value} onChange={handleChange}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
}
请花点时间理解上面的代码。如果您不熟悉泛型类型,它看起来可能很奇怪。
你可能会问,为什么我们要定义它OptionValue,然后再把它放到extends OptionValue很多地方。
假设我们不这样做,而是Type extends OptionValue直接使用 `is` Type。组件如何Select知道类型Type只能是 `a`number或 `b` string,而不能是其他类型呢?
不可能。所以我们必须说:“嘿,这个Type东西既可以是字符串,也可以是数字”。
这虽然与泛型无关,但如果你在实际的编辑器中使用上面的代码,你可能会在handleChange函数内部遇到 TypeScript 错误。
原因在于,event.target.value即使它原本是数字,也会被转换成字符串。而useState该函数期望的类型Type是字符串,它可以是数字。所以这里存在问题。
我发现处理这个问题的最佳方法是使用选中元素的索引,如下所示:
function handleChange(event: ChangeEvent<HTMLSelectElement>) {
setValue(options[event.target.selectedIndex].value);
}
包起来
希望这篇文章能帮助你更好地理解泛型类型的工作原理。当你真正了解它们之后,它们就不会那么可怕了😊
是的,TypeScript 的语法确实需要一些时间才能习惯,而且也不太美观。但是,泛型是 TypeScript 工具箱中不可或缺的一部分,它能帮助你创建优秀的 TypeScript React 应用,所以不要仅仅因为这一点就排斥它。
尽情享受开发应用的乐趣吧!
PS:React 中还有其他我应该在本文中提及的通用类型应用程序吗?如果有,请随时在Twitter上联系我,或发送电子邮件至pierre@devtrium.com。
文章来源:https://dev.to/pierreouannes/react-typescript-use-generics-to-improve-your-types-ha8