继续创造,加快生长!这是我参加「日新方案 10 月更文挑战」的第19天,点击检查活动概况
浅谈 React 和 TypeScript 开发中的泛型实践
泛型是 TypeScript 中的一个重要部分:它们看起来很古怪,它们的目的不明显,并且它们或许很难分析。本文旨在帮助你了解和揭开 TypeScript 泛型的神秘面纱,特别是它们在 React 中的运用。它们并没有那么杂乱:假如你了解函数,那么泛型也就不远了。
1. TypeScript中的泛型是什么?
要了解泛型,咱们首先从比较标准 TypeScript 类型和 JavaScript 目标开端。
// JavaScript 目标
const user = {
name: 'John',
status: 'online',
};
// 对应的 TypeScript 类型
type User = {
name: string;
status: string;
};
如你所见,十分像。主要的区别是,在 JavaScript 中你关怀的是变量的值,而在 TypeScript 中你关怀的是变量的类型。
关于 User
类型,咱们能够说的一点是它的 status
属性太模糊了。状况通常有预界说的值,比如在本例中,它能够是 "online"
或 "offline"
。咱们能够修改咱们的类型:
type User = {
name: string;
status: 'online' | 'offline';
};
但前提是咱们现已知道有哪些状况。假如咱们不这样做,而实践的状况列表发生了变化呢?这便是泛型的用武之地:它们允许你指定能够根据用法更改的类型。
咱们将在后面看到怎么完成这个新类型,但是对于咱们的 User
示例,运用泛型类型看起来像这样:
// `=User 现在是泛型类型
const user: User<'online' | 'offline'>;
// 假如咱们需要,咱们能够很简单地添加一个新的状况 "idle"
const user: User<'online' | 'offline' | 'idle'>;
上面说的是“用户变量是一个 user
类型的目标,顺便说一下,该用户的状况选项要么是 'online'
,要么是 'offline'
”(在第二个示例中,你将 'idle'
添加到该列表中)。
下面是怎么完成这种类型:
// 泛型类型界说
type User<StatusOptions> = {
name: string;
status: StatusOptions;
};
StatusOptions
被称为“类型变量”,而 User
被称为“泛型类型”。
你或许会觉得很古怪。但这仅仅一个函数。假如我运用相似 JavaScript 的语法(不是有用的 TypeScript )来编写它,它看起来像这样:
type User = (StatusOption) => {
return {
name: string;
status: StatusOptions;
}
}
正如你所看到的,它实践上仅仅函数的 TypeScript 等价物。你能够用它做一些很有意思的工作。例如,假定咱们的 User
承受一个 status
数组,而不是像曾经那样承受一个 status
。对于泛型类型,这依然十分简单做到:
// 界说类型
type User<StatusOptions> = {
name: string;
status: StatusOptions[];
};
// 类型的用法依然相同
const user: User<'online' | 'offline'>;
假如你想了解更多关于泛型的常识,你能够检查 TypeScript 的攻略。
2. 为什么泛型十分有用?
现在你知道了泛型类型是什么以及它们是怎么工作的,你或许会问自己为什么需要它。毕竟,咱们上面的比如中,你能够界说一个类型 Status
并运用它:
type Status = 'online' | 'offline';
type User = {
name: string;
status: Status;
};
在这个(适当简单的)比如中是这样的,但是在很多状况下你不能这样做。通常状况下,当你期望在多个实例中运用一个共享类型时,每个实例都有一些不同:你期望该类型是动态的,并习惯其运用办法。
一个十分常见的比如是函数回来与其实参相同的类型。最简单的办法是 identity
函数,它回来给定的任何值:
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 能够推断出类型本身,但有时它不能(或做错了),你有必要自己指定类型。
3. 多个类型变量
你并不局限于一个类型变量,你能够运用恣意多个类型变量。例如:
function identities<ArgType1, ArgType2>(
arg1: ArgType1,
arg2: ArgType2
): [ArgType1, ArgType2] {
return [arg1, arg2];
}
在这个实例中,identity
承受两个参数并以数组办法回来它们。
4. JSX 中箭头函数的泛型语法
你或许现已注意到,我现在只运用了惯例函数语法,而没有运用 ES6 中引入的箭头函数语法。
// 一个箭头函数
const identity = (arg) => {
return arg;
};
原因是 TypeScript 处理箭头函数的才能不如惯例函数(当运用 JSX 时)。你或许认为你能够这样做:
// 这个不可
const identity<ArgType> = (arg: ArgType): ArgType => {
return arg;
}
// 这个也不可
const identity = <ArgType>(arg: ArgType): ArgType => {
return arg;
}
但这在 TypeScript 中不起作用。相反,你能够通过以下办法编写:
const identity = <ArgType,>(arg: ArgType): ArgType => {
return arg;
};
// or
const identity = <ArgType extends unknown>(arg: ArgType): ArgType => {
return arg;
};
我主张运用第一个,由于它更简洁,但逗号在我看来还是有点古怪。需要清晰的是,这个问题源于咱们在运用 TypeScript 和 JSX(被称为 TSX)。在一般的 TypeScript 中,你不必运用这个解决办法。
5. 关于类型变量名的正告
出于某种原因,在 TypeScript 国际中,泛型类型中的类型变量的称号通常是一个字母。
// 看到的不是这个
function identity<ArgType>(arg: ArgType): ArgType {
return arg;
}
// 你通常会看到这个
function identity<T>(arg: T): T {
return arg;
}
运用完整的单词作为类型变量名确实会使代码适当冗长,但我依然认为这比运用单字母选项更简单了解。
6. 开源的泛型类型示例 — useState
接下来让咱们看看 React 库中 useState
的泛型类型。
注:此部分比本文的其他部分要杂乱一些。假如你一开端不明白,能够稍后再看。
让咱们来看看 useState
的类型界说:
function useState<S>(
initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>];
让咱们一步一步地了解这个类型界说:
- 咱们首先界说一个函数
useState
,它承受一个名为S
的泛型类型。 - 该函数只承受一个参数:
initialState
。- 初始状况能够是类型为
S
的变量(咱们的泛型类型),也能够是回来类型为S
的函数。
- 初始状况能够是类型为
- 然后
useState
回来一个包含两个元素的数组:- 第一个类型是
S
(它是咱们的状况值)。 - 第二个是
Dispatch
类型,运用泛型类型SetStateAction
。SetStateAction
本身是运用泛型类型S
的SetStateAction
类型(它是咱们的状况setter
)。
- 第一个类型是
最终一部分有点杂乱,所以让咱们进一步研究一下。
首先,让咱们检查 SetStateAction
:
type SetStateAction<S> = S | ((prevState: S) => S);
SetStateAction
也是一个泛型类型它能够是类型为 S
的变量,也能够是一个参数类型和回来类型都为 S
的函数。
这让我想起 setState
供给了什么?你能够直接供给新的状况值,也能够供给一个函数,根据旧的状况值构建新的状况值。
Dispatch
是什么?
type Dispatch<A> = (value: A) => void;
这个有一个参数是泛型类型,什么都不回来。
把它们放在一起:
// 原类型
type Dispatch<SetStateAction<S>>
// 能够被重构为这个类型
type (value: S | ((prevState: S) => S)) => void
这个函数要么承受值 S
要么承受值 S => S
,然后什么都不回来。这确实与咱们对 setState
的运用相匹配。
这便是 useState
的整个类型界说。现在,实践上该类型是重载的(这意味着根据上下文或许运用其他类型界说),但这是主要的一个。另一个界说只处理没有给 useState
参数的状况,因此 initialStat
e 是未界说的。
function useState<S = undefined>(): [
S | undefined,
Dispatch<SetStateAction<S | undefined>>
];
7. 在 React 中运用泛型
既然咱们现已了解了 TypeScript 泛型类型的一大致概念,让咱们来看看怎么实践于 React 的开发中。
7.1 像 useState 这样的 hook 的泛型类型
hook 仅仅一些一般的 JavaScript 函数,React 对它们的处理略有不同。由此可见,泛型类型与 hook 的运用与一般 JavaScript 函数的运用是相同的:
// 一般的 JavaScript 函数
const greeting = identity<string>('Hello World');
// useState
const [greeting, setGreeting] = useState<string>('Hello World');
在上面的比如中,你能够省略显式泛型类型,由于 TypeScript 能够从参数值推断出它。但有时候 TypeScript 做不到(或者做错了),这便是要运用的语法。
7.2 组件 prop 的泛型类型
假定你正在为表单构建一个 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
const mockOptions = [
{ value: 'banana', label: 'Banana ' },
{ value: 'apple', label: 'Apple ' },
{ value: 'coconut', label: 'Coconut ' },
{ value: 'watermelon', label: 'Watermelon ' },
];
function Form() {
return <Select options={mockOptions} />;
}
假定对于选项的值,咱们能够承受字符串或数字,但不能同时承受两者。怎么在 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>
);
}
它不起作用的原因是,在一个选项数组中,或许有一个选项的值类型为 number
,而另一个选项的值类型为 string
。咱们不期望这样,但 TypeScript 会承受它。
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
咱们仅仅用 Type
来替代。Select
组件怎么知道类型 Type
能够是数字或字符串,而不是其他类型?
注 假如你在实践的编辑器中运用上述代码,你或许会在
handleChange
函数中得到一个 TypeScript 错误。这样做的原因是event.target.value
将被转换为字符串,即使它是一个数字。useState
期望类型Type
,它能够是一个数字。
我发现处理这个问题的最好办法是运用所选元素的索引,像这样:
function handleChange(event: ChangeEvent<HTMLSelectElement>) {
setValue(options[event.target.selectedIndex].value);
}
8. 小结
我期望本文能帮助你更好地了解泛型类型是怎么工作的。当你了解他们,他们不再那么可怕。泛型是 TypeScript 工具箱中创建优秀 TypeScript React 运用程序的重要组成部分,所以不要回避它们。