发布于 2026-01-06 0 阅读
0

React & TypeScript:使用泛型改进你的类型 DEV 的全球展示挑战赛,由 Mux 呈现:展示你的项目!

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;
};
Enter fullscreen mode Exit fullscreen mode

如你所见,两者非常接近。主要区别在于,JavaScript 关注的是变量的值,而 TypeScript 关注的是变量的类型。

我们可以看出,我们定义的User类型存在一个问题,那就是它的status属性过于模糊。状态通常具有预定义的值,例如,在本例中,它可以是“在线”或“离线”。我们可以修改我们的类型:

type User = {
  name: string;
  status: 'online' | 'offline';
};
Enter fullscreen mode Exit fullscreen mode

但这假设我们已经知道有哪些状态类型。如果我们不知道,而且实际的状态列表发生了变化怎么办?这就是泛型的作用所在:它们允许你指定一个可以根据使用情况而变化的类型

稍后我们将探讨如何实现这种新类型,但对于我们的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'>;
Enter fullscreen mode Exit fullscreen mode

上面的意思是“该user变量是 类型的对象User,顺便说一下,该用户的状态选项为‘在线’或‘离线’”(在第二个示例中,您将‘空闲’添加到该列表中)。

好吧,带尖括号的语法< >看起来确实有点怪。我同意。不过习惯就好。

是不是很酷?现在我们来看看如何实现这种类型:

// generic type definition
type User<StatusOptions> = {
  name: string;
  status: StatusOptions;
};
Enter fullscreen mode Exit fullscreen mode

StatusOptions它被称为“类型变量”,并且User被认为是“泛型类型”。

你可能觉得它看起来很奇怪。但这实际上只是一个函数!如果我用类似 JavaScript 的语法(不是有效的 TypeScript 语法)来编写它,它看起来会像这样:

type User = (StatusOption) => {
  return {
    name: string;
    status: StatusOptions;
  }
}
Enter fullscreen mode Exit fullscreen mode

如你所见,它实际上就是 TypeScript 中函数的等效物。而且你可以用它做很多很酷的事情。

例如,假设我们User接受的不是像以前那样单个状态,而是一个状态数组。使用泛型类型仍然很容易实现这一点:

// defining the type
type User<StatusOptions> = {
  name: string;
  status: StatusOptions[];
};

// the type usage is still the same
const user: User<'online' | 'offline'>;
Enter fullscreen mode Exit fullscreen mode

如果你想了解更多关于泛型的知识,可以查看TypeScript 的相关指南

为什么泛型非常有用

既然你已经了解了泛型类型及其工作原理,你可能会问自己为什么需要它。毕竟,上面的例子相当人为:你可以定义一个类型Status并使用它:

type Status = 'online' | 'offline';

type User = {
  name: string;
  status: Status;
};
Enter fullscreen mode Exit fullscreen mode

在这个(相当简单的)例子中确实如此,但在很多情况下,这样做是行不通的。通常情况下,当你想在多个实例中使用同一个类型,而每个实例又有所不同时,就会出现这种情况:你希望这个类型是动态的,能够根据它的使用方式进行调整。

一个非常常见的例子是,函数返回的数据类型与其参数类型相同。最简单的形式是恒等函数,它返回给定的任何类型:

function identity(arg) {
  return arg;
}
Enter fullscreen mode Exit fullscreen mode

很简单,对吧?但是如果参数可以是任何类型,该怎么写呢arg?别跟我说用any!

没错,就是仿制药:

function identity<ArgType>(arg: ArgType): ArgType {
  return arg;
}
Enter fullscreen mode Exit fullscreen mode

我再次发现这种语法有点复杂,难以理解,但它实际上只是在说:“该identity函数可以接受任何类型(ArgType),并且该类型既是其参数的类型,也是其返回类型的类型”。

以下是如何使用该函数并指定其类型:

const greeting = identity<string>('Hello World!');
Enter fullscreen mode Exit fullscreen mode

在这个特定情况下,<string>由于 TypeScript 可以自行推断类型,所以没有必要这样做,但有时它无法推断(或者推断错误),这时你就必须自己指定类型。

多类型变量

你不局限于使用一种类型的变量,你可以使用任意多种类型。例如:

function identities<ArgType1, ArgType2>(
  arg1: ArgType1,
  arg2: ArgType2
): [ArgType1, ArgType2] {
  return [arg1, arg2];
}
Enter fullscreen mode Exit fullscreen mode

在这种情况下,identities它接受 2 个参数并将它们以数组的形式返回。

JSX 中箭头函数的泛型语法

你可能已经注意到,目前我只使用了常规函数语法,而没有使用 ES6 中引入的箭头函数语法。

// an arrow function
const identity = (arg) => {
  return arg;
};
Enter fullscreen mode Exit fullscreen mode

原因在于 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;
}
Enter fullscreen mode Exit fullscreen mode

但这在 TypeScript 中行不通。你需要执行以下操作之一:

// use this
const identity = <ArgType,>(arg: ArgType): ArgType => {
  return arg;
}

// or this
const identity = <ArgType extends unknown>(arg: ArgType): ArgType => {
  return arg;
}
Enter fullscreen mode Exit fullscreen mode

我建议使用第一种方案,因为它更简洁,但我觉得逗号看起来还是有点奇怪。

需要说明的是,这个问题源于我们使用了 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;
}
Enter fullscreen mode Exit fullscreen mode

使用完整的单词作为类型变量名确实会使代码变得相当冗长,但我仍然认为它比使用单个字母选项更容易理解。

我鼓励你在通用名称中使用实际的单词,就像你在代码其他地方所做的那样。但请注意,在实际应用中,你很可能会经常看到单字母版本。

额外福利:来自开源软件的通用类型示例:useState它本身!

为了总结关于泛型类型的这一部分,我想不妨看看泛型类型的实际应用案例。还有什么比 React 库本身更好的例子呢?

友情提示:本节内容比本文其他部分略微复杂一些。如果您一开始没理解,可以稍后再阅读。

让我们来看一下我们心爱的 hook 的类型定义useState

function useState<S>(
  initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>];
Enter fullscreen mode Exit fullscreen mode

你不能说我没警告过你——使用泛型的类型定义并不美观。或许只有我这么觉得吧!

总之,让我们一步一步地理解这个类型定义:

  • 我们首先定义一个函数,useState它接受一个名为的泛型类型S
  • 该函数接受且仅接受一个参数:一个initialState
    • S初始状态可以是类型为(我们的通用类型)的变量,也可以是返回类型为的函数S
  • useState然后返回一个包含两个元素的数组:
    • 第一个是类型S(这是我们的状态值)。
    • 第二个类型是应用了Dispatch泛型类型的类型SetStateAction<S>SetStateAction<S>它本身就是应用了SetStateAction泛型类型的类型S(它是我们的状态设置器)。

最后一部分有点复杂,所以我们再深入探讨一下。

首先,我们来查一下SetStateAction

type SetStateAction<S> = S | ((prevState: S) => S);
Enter fullscreen mode Exit fullscreen mode

好的,所以SetStateAction它也是一个通用类型,它可以是类型为的变量,也可以是参数类型和返回类型都为的S函数。S

这让我想起了我们提供给它的服务setState,对吧?您可以直接提供新的状态值,也可以提供一个函数,该函数根据旧状态值构建新的状态值。

现在是什么Dispatch

type Dispatch<A> = (value: A) => void;
Enter fullscreen mode Exit fullscreen mode

好的,这个函数只有一个参数,参数类型为泛型类型,并且不返回任何值。

综合起来:

// this type:
type Dispatch<SetStateAction<S>>

// can be refactored into this type:
type (value: S | ((prevState: S) => S)) => void
Enter fullscreen mode Exit fullscreen mode

所以这是一个接受一个值S或一个函数作为参数S => S,但不返回任何值的函数。

这确实符合我们对setState.

这就是 `!` 的完整类型定义。useState实际上,该类型是重载的(这意味着根据上下文,可能适用其他类型定义),但这是主要的。另一个定义只处理不给 `!` 传递任何参数的情况useState,所以initialState是` undefined!`。

供您参考:

function useState<S = undefined>(): [
  S | undefined,
  Dispatch<SetStateAction<S | undefined>>
];
Enter fullscreen mode Exit fullscreen mode

在 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');
Enter fullscreen mode Exit fullscreen mode

在上面的示例中,您可以省略显式的泛型类型,因为 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} />;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

之所以行不通,是因为在一个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 🍉' },
];
Enter fullscreen mode Exit fullscreen mode

强制要求输入数字或整数的方法使用泛型

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

请花点时间理解上面的代码。如果您不熟悉泛型类型,它看起来可能很奇怪。

你可能会问,为什么我们要定义它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);
}
Enter fullscreen mode Exit fullscreen mode

包起来

希望这篇文章能帮助你更好地理解泛型类型的工作原理。当你真正了解它们之后,它们就不会那么可怕了😊

是的,TypeScript 的语法确实需要一些时间才能习惯,而且也不太美观。但是,泛型是 TypeScript 工具箱中不可或缺的一部分,它能帮助你创建优秀的 TypeScript React 应用,所以不要仅仅因为这一点就排斥它。

尽情享受开发应用的乐趣吧!

PS:React 中还有其他我应该在本文中提及的通用类型应用程序吗?如果有,请随时在Twitter上联系我,或发送电子邮件至pierre@devtrium.com

文章来源:https://dev.to/pierreouannes/react-typescript-use-generics-to-improve-your-types-ha8