likes
comments
collection
share

【React】一些实际项目中的 TypeScript 技巧(一)~

作者站长头像
站长
· 阅读数 4

在React项目中熟练使用TypeScript已经是每个前端开发人员必备的技能,如果你还在用“any”一把搜哈,那你可能不需要 TypeScript!

TypeScript有神好处呢?它可以在编译时捕获许多常见的错误,如类型不匹配、属性不存在等。这可以大大减少在运行时出现的错误,并提高代码的可靠性和稳定性。通过为函数、接口、类型和变量添加类型注解,可以使代码更加可读,使其他开发人员更容易理解你的意图,这对于团队合作和维护代码库非常有帮助。

1. 枚举类型

在 TypeScript 中,要获取枚举(enum)的键(key)的类型,可以使用 keyof 关键字结合类型引用来实现。下面是一个示例:

enum MyEnum {
  Key1 = 'Value1',
  Key2 = 'Value2',
  Key3 = 'Value3',
}

type MyEnumKeys = keyof typeof MyEnum;

// MyEnumKeys 的类型为 "Key1" | "Key2" | "Key3"

keyof typeof MyEnum 表达式返回了 MyEnum 枚举的键的类型。在这个例子中,MyEnumKeys 的类型将是 "Key1" | "Key2" | "Key3"。你可以根据需要将这个类型用于其他用途,比如函数参数、变量声明等。

注意,使用 keyof 运算符获取的是枚举的键的类型,而不是枚举成员的值的类型。如果你需要获取枚举成员的值的类型,可以使用索引访问类型(index access type)来实现。例如:

enum MyEnum {
  Key1 = 'Value1',
  Key2 = 'Value2',
  Key3 = 'Value3',
}

type MyEnumValues = MyEnum[keyof typeof MyEnum];

// MyEnumValues 的类型为 "Value1" | "Value2" | "Value3"

MyEnumValues 的类型将是 "Value1" | "Value2" | "Value3",它表示了枚举成员的值的类型。

2. 将 JSX 作为 Prop 传递

React 的 Prop 非常强大,可以将组件作为 Prop 传递,可以使组件更具可重用性。将组件作为道具传递的最灵活方法之一就是让组件接收 JSX。来看看下面的例子:

interface LayoutProps {
  nav: React.ReactNode;
  children: React.ReactNode;
}
 
const Layout = (props: LayoutProps) => {
  return (
    <>
      <nav>{props.nav}</nav>
      <main>{props.children}</main>
    </>
  );
};
 
// 使用
<Layout nav={<h1>My Site</h1>}>
  <div>Hello!</div>
</Layout>;

我们将 props 定义为 React.ReactNode 类型,这是一种可接受任何有效 JSX 的类型。请注意,我这里没有使用 React.ReactElementJSX.Element

3. 将组件作为 Prop 传递

第二种方法是,我们不把 JSX 作为 props 传入,而是把整个组件作为 props 传入。

需要注意的是:JSX 是组件返回的内容,<Wrapper /> 是 JSX。Wrapper 是组件。

在 TypeScript 中,最简单方法是使用 React.ComponentType:

const Row = (props: {
  icon: React.ComponentType<{
    className?: string;
  }>;
}) => {
  return (
    <div>
      <props.icon className="h-8 w-8" />
    </div>
  );
};
 
<Row icon={UserIcon} />;

在这里,我们将图标组件定义为 React.ComponentType 类型。我们将 { className?: string } 传递给 React.ComponentType,表示这是一个可以接收 className 的组件。

这基本上表示图标可以是任何可以接收 className 的组件。这是一种非常灵活的类型,而且易于使用。

4. 将原生标签作为 Prop 传递

使用 React.ElementType 可以将本地标签作为 props 或自定义组件传递。

const Row = (props: {
  element: React.ElementType<{
    className?: string;
  }>;
}) => {
  return (
    <div>
      <props.element className="h-8 w-8" />
    </div>
  );
};
 
<Row element={"div"} />;
<Row element={UserIcon} />;

这是一个非常灵活的定义,而且非常容易使用。我们甚至可以自动完成传递给元素的所有选项。

我会倾向于将 JSX 作为 props 传递。它不仅容易定义类型(React.ReactNode),而且对性能非常友好。当父组件重新渲染时,作为道具传递给组件的 JSX 不会重新渲染。这可以极大地提升性能。

5. 使用新特性 'Satisfies'

satisfies 运算符提供了一种为值添加类型注解而不丢失值推理的方法。

5.1 使用 satisfies 的强类型 URLSearchParams

satisfies 非常适合强类型化函数,这些函数通常使用更宽松的类型。

在使用 URLSearchParams 时,它的参数通常是 Record<string,string>。这是一种非常松散的类型,并不强制执行任何特定的键。

但通常情况下,你需要创建一些搜索参数并将它们传递给 URL。因此,这种松散的类型最终会变得相当危险。

这时,satisfies 来救你了。你可以使用它来对 params 对象进行强类型内联。

【React】一些实际项目中的 TypeScript 技巧(一)~

在这里,我们会收到一个错误信息,提示我们缺少一个属性正文。这很好,因为这意味着我们不会意外创建一个没有正文的 URL。

5.2 带 satisfies 的强类型 POST 请求

在发出 POST 请求时,向服务器发送正确的数据结构非常重要。服务器会期望请求正文有特定的格式,但使用 JSON.stringify 将其转化为 JSON 的过程会消除任何强类型。

但通过 satisfies 操作符,我们可以对其进行强类型。

type Post = {
  title: string;
  content: string;
};
 
fetch("/api/posts", {
  method: "POST",
  body: JSON.stringify({
    title: "New Post",
    content: "Lorem ipsum.",
  } satisfies Post),
});

在这里,我们可以用 Post 类型注释请求正文,确保标题和内容属性存在且类型正确。

5.3 用 satisfies 而不是 as const 来推断元组

通常,你会希望在 TypeScript 中声明一个元素数组,但将其推断为元组而非数组。你可能会使用 as const 断言来推断元组类型,而不是数组类型。然而,使用 satisfies 操作符,你可以在不使用 as const 的情况下获得相同的结果。

type MoreThanOneMember = [any, ...any[]];
const array = [1, 2, 3];
//    ^?
const maybeExists = array[3];
//    ^?
const tuple = [1, 2, 3] satisfies MoreThanOneMember;
//    ^?
const doesNotExist = tuple[3];

在上面的代码中,我们以两种不同的方式声明数组。如果我们不使用 satisfies 对其进行注解,它就会被推断为 number[]。这意味着当我们尝试访问数组中不存在的元素时,TypeScript 不会给出错误信息;它只是将其推断为 number | undefined

然而,当我们使用 satisfies 操作符声明元组时,它会推断出该类型是一个包含三个元素的元组。现在,当我们尝试使用 tuple[3] 访问第四个元素时,TypeScript 正确地给出了一个错误,因为索引超出了边界。

5.4 使用 satisfies 强制 as const 对象成为某种类型

使用 as const 时,我们可以指定将对象视为具有字面类型的不可变值。但是,这并不能强制对象具有任何特定的形状或属性。要强制 as const 对象具有特定形状,我们可以使用 satisfies 操作符。

在下面的示例中,我们有一个 RouteObject 类型,它代表了一个路由集合。每个路由都有一个字符串类型的 url 属性和一个可选的 searchParams 属性。我们要确保路由对象满足 RouteObject 类型。

【React】一些实际项目中的 TypeScript 技巧(一)~

这不仅能在属性丢失的情况下为我们提供极大的错误提示,还能为路由对象提供自动完成功能。

5.5 使用 satisfies 强制 as const 数组为特定类型

同时使用 satisfiesas const 和数组可能有点麻烦。

举个例子,我们有一个导航菜单,它由带有标题的元素、可选的 URL 以及在 children 属性下嵌套导航元素的可选数组组成。

type NavElement = {
  title: string;
  url?: string;
  children?: readonly NavElement[];
};
 
const nav = [
  {
    title: "Home",
    url: "/",
  },
  {
    title: "About",
    children: [
      {
        title: "Team",
        url: "/about/team",
      },
    ],
  },
] as const satisfies readonly NavElement[];

现在,如果我们试图访问不属于已定义形状的属性,TypeScript 会给出错误信息。

【React】一些实际项目中的 TypeScript 技巧(一)~

readonly 数组中使用 satisfies

需要注意的是在数组中使用了 readonly。如果子数组上没有 readonly,TypeScript 就会出错:

【React】一些实际项目中的 TypeScript 技巧(一)~

这是因为 NavElement[] 是可变的,所以需要标记为 readonly 以与 const 匹配。

如果我们漏掉最后的 readonly,情况也是一样:

【React】一些实际项目中的 TypeScript 技巧(一)~

这是因为我们的外部类型 NavElement[] 是可变的,因此不能赋值给 readonly as const 声明。

6. Array<T> vs T[]: 哪个更好?

在 TypeScript 中声明数组类型时,有两种选择:Array<T>T[]React Query 的维护者之一 Dominik (@TKDodo) 最近发布了一篇文章,介绍了应该选择哪种方式。他强烈主张使用 Array<T>

  • 实际上,Array<T> 和 T[] 在功能上是相同的:
const firstTest = (arr: Array<string>) => {};
 
const secondTest = (arr: string[]) => {};
 
// 是一样的
firstTest(["hello", "world"]);
secondTest(["hello", "world"]);
  • 但,在 T[] 使用 keyof 将会报错:

【React】一些实际项目中的 TypeScript 技巧(一)~

解决办法是使用 Array<T>

const result: Array<keyof Person> = ["id", "name"];
  • Dominik 认为 Array<string>string[] 更好读。这很主观,就像读 "字符串数组"(array of strings)和 "字符串数组"(string array)的区别一样。

  • 在悬停值或显示错误时,TypeScript 使用 T[] 语法。没有经验的 TS 开发人员在转换代码中的 Array<T> 和错误中的 T[] 时,可能会产生认知负担。

【React】一些实际项目中的 TypeScript 技巧(一)~

  • 总的来说,Dominik 的观点(即 Array<T> 总是更好的选择)并不完全正确。无论是哪种方法,都有足够多的注意事项,因此我不会做出这样或那样的推荐。
  • 但是,你应该保持一致。可以使用 ESLint 规则 在代码库中强制使用其中一种方法。如果让我选择,我会选择 T[]

6.1 无功能上的差异

很多开发人员喜欢语法上的争论,尤其是当两个选项在功能上差别不大的时候。如上所述,Array<T>T[] 的行为完全相同,但有一个小例外。

【React】一些实际项目中的 TypeScript 技巧(一)~

在这里,当我们尝试在休息位置使用 T[] 语法时,会出现错误。但正如本 PR 所示,在未来的 TS 版本中,即使是这种行为也可能会消失。因此,我们可以将两者视为功能相同。

6.2 keyof

如果要确定使用哪种语法,就需要考虑 keyof 操作符。如上所述,使用 T[]keyof 可能会导致意想不到的结果。

【React】一些实际项目中的 TypeScript 技巧(一)~

在这里,你会认为 keyof Person 会在 [] 操作符启动之前解析,这意味着你最终会得到一个类似 ('id' | 'name')[] 的类型。但不幸的是,[] 首先解析,所以你最终在 Person[] 上执行了 keyof

你可以用括号将 keyof Person 包起来来解决这个问题:

const result: (keyof Person)[] = ["id", "name"];

或者,你用 Array<T> 代替:

【React】一些实际项目中的 TypeScript 技巧(一)~

6.3 可读性

Dominik 认为 Array<T>T[] 更可读。你可能会同意这个观点,但我认为这是主观臆断。

6.3.1 Readonly Arrays

如果您想与 Array<T> 保持一致,您可能还想使用 ReadonlyArray<T> 类型:

【React】一些实际项目中的 TypeScript 技巧(一)~

readonly T[] 比较:

【React】一些实际项目中的 TypeScript 技巧(一)~

您更喜欢哪一种?我觉得这个很难区分。

6.3.2 多维数组

在处理数组的数组时,您还需要考虑 Array<Array<T>>:

const array: Array<Array<string>> = [
  ["hello", "world"],
  ["foo", "bar"],
];

比较 T[][] 写法:

const array2: string[][] = [
  ["hello", "world"],
  ["foo", "bar"],
];

哪个更好?

6.4 TypeScript Uses T[]

TypeScript 确实对它更喜欢哪种语法提出了自己的看法。在悬停和错误中,TypeScript 将始终使用 T[] 语法。

【React】一些实际项目中的 TypeScript 技巧(一)~

这意味着,如果您在代码中使用 Array<T>,经验不足的 TypeScript 开发人员在两种语法之间转换时会遇到一些认知负担。

这就是为什么至少对我来说,T[] 感觉更自然的一个重要原因 —— 它更多地存在于语言中,受到编译器的支持,并且在文档中随处可见。