浅谈 React 和 TypeScript 开发中的泛型实践

继续创造,加快生长!这是我参加「日新方案 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 类型,运用泛型类型 SetStateActionSetStateAction 本身是运用泛型类型 SSetStateAction 类型(它是咱们的状况 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 参数的状况,因此 initialState 是未界说的。

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 运用程序的重要组成部分,所以不要回避它们。