使用TypeScript并升级到React 18

2,446 阅读4分钟

原文:blog.logrocket.com/upgrading-r…

为了支持React 18,React类型定义进行了升级,其中包含了一些break change。本文将讲述在TypeScript中如何升级到React 18

React 18和Definitely Typed

在alpha和beta测试经历了相当长的一段时间后,React 18 于2022年3月29日正式发布。在第一个alpha版本发布的时候,TypeScript就提供了支持

这是通过Definitely Typed(一个社区维护的各种TypeScript类型定义的库)的类型定义实现的)来使用。感谢Sebastian Silbermann的贡献,他在React18的类型定义工作中投入了大量的精力

目前React 18已经发布并且React 18 的类型定义在 Sebastian 的pr合并后也进行了更新。许多项目会面临一些break change。本文章将介绍会产生哪些break change及如何解决

Definitely Typed和语义版本控制

开发者习惯于在使用的软件中进行语义版本控制。通常来说在主版本的修改是表明有重大更改的。这正是React从v17升级到v18所做的事

Definitely Typed是不支持语义版本控制的

这不是故意的。因为Definitely Typed特意将类型定义发布到npm的@types作用域下。例如,React的类型定义被发布到@types/react

需要注意的是,npm 建立在语义版本控制之上。为了使类型定义的使用更容易,类型定义包的版本将等同于它支持的 npm 包的版本。对于 react18.0.0,对应的类型定义是@types/react18.0.0

如果@types/react类型定义发生breaking change,则会发布新版本而不是增加主要或次要版本号

修改将仅应用于修订号。这样做是为了通过npm维护当前更简单的类型消费模型

React 18: 类型上的breaking change

综上所述,对于那些被广泛使用的类型定义包,都会尽量减少产生breaking change

顺便说一句,Definitely Typed自动化工具将类型定义分为三类: "深受大家喜爱(Well-liked by everyone)"、"流行(Popular)"和"关键(Critical)"。感谢Andrew Branch的分享。被广泛使用的React被认为是"关键的"

当Sebastian提交了一个pr来升级TypeScript的React类型定义时,就有机会来做一些重大的修改。这些修改可能并不都与React 18有直接关系但会修复React类型定义中长期存在的一些问题

Sebastian pr非常好,我建议你去看一下。以下是重大更改的摘要

  • 移除隐式children
  • 移除ReactFragment中的{}(related to 1.)
  • this.context变成unkown
  • Using noImplicitAny now enforces a type is supplied with useCallback
  • noImplicitAny应用到useCallback
  • 删除不推荐使用的类型与React官方保持一致

在上述修改中,移除隐式children是最具破坏性的。Sebastian专门写了一篇博客来解释其原因。他还写了一个codemod来有利于进行这个代码迁移

下面让我们开始将代码库的react升级到18吧!

升级

我将通过升级我阿姨的网站进行演示。这是一个简单的网站,升级的pr

首先在package.json中升级React

-    "react": "^17.0.0",
-    "react-dom": "^17.0.0",
+    "react": "^18.0.0",
+    "react-dom": "^18.0.0",

然后升级类型定义

-    "@types/react": "^17.0.0",
-    "@types/react-dom": "^17.0.0",
+    "@types/react": "^18.0.0",
+    "@types/react-dom": "^18.0.0",

升级的时候需要检查lock依赖(yarn.lock / package-lock.json等),确保只有18版本的@types/reactreact

现在依赖安装已完成,会看到以下报错

Property ‘children’ does not exist on typeLoadingProps’.ts(2339)

代码如下

interface LoadingProps {
  // 你会注意到这里没有 `children` 属性 - 这就是出现错误的原因
  noHeader?: boolean;
}
// if props.noHeader is true then this component returns just the icon and a message
// if props.noHeader is true then this component returns the same but wrapped in an h1
const Loading: React.FunctionComponent<LoadingProps> = (props) =>
  props.noHeader ? (
    <>
      <FontAwesomeIcon icon={faSnowflake} spin /> Loading {props.children} ...
    </>
  ) : (
    <h1 className="loader">
      <FontAwesomeIcon icon={faSnowflake} spin /> Loading {props.children} ...
    </h1>
  );

在这里看到的是去除隐式children的改动。在我们进行升级之前,所有React.ComponentReact.FunctionComponent都有一个children属性,它允许React用户在不声明children的情况下直接使用

升级18后就不一样了。如果有一个带有子组件,则必须显式声明这个组件的类型

在这个例子中,通过直接添加children属性的声明可以修复这个问题

interface LoadingProps {
  noHeader?: boolean;
  children: string;
}

但是,当可以让其他方式帮我们写代码的话,为什么还要写代码呢?

我们可以使用Sebastian开发的codemod来替代手动修改代码。使用它直接通过以下的命令就可以:

npx types-react-codemod preset-18 ./src

执行后,会看到如下提示:

选择a并让codemod运行。对于这个项目,有37个文件更新了。所有文件都需要进行相同的修改。在每种情况下,组件的props都被React.PropsWithChildren包起来。例如Loading组件如下

-const Loading: React.FunctionComponent<LoadingProps> = (props) =>
+const Loading: React.FunctionComponent<React.PropsWithChildren<LoadingProps>> = (props) =>

PropsWithChildren仅仅是将children属性添加,如下

type PropsWithChildren<P> = P & { children?: ReactNode | undefined };

这就解决了上面遇到的编译问题,没有类型问题报错了

总结

通过本文我们已经学习到React 18是如何出现类型的破坏性更改,并知道可以使用codemod快速进行升级