学习用TypeScript和React来创建应用程序

56 阅读3分钟

我目前和几个React的新人一起工作,教他们用TypeScript和React来创建应用程序。这很有趣,对于已经使用了一段时间的我来说,这是用新的眼光看待这块技术的好方法。

看到他们中的一些人以你从未设想过的方式使用React,这也很好。不太好的是,如果你遇到了React抛出错误(可能会使你的应用程序崩溃)的情况,而TypeScript甚至没有退缩。最近就发生了这样的情况,而且我担心不会有一个简单的解决方法。

问题#

考虑一下下面这个组件。这是一个卡片,它接受一个标题并渲染任意的孩子。我使用我自己的WithChildren 辅助类型(见React模式),但如果你使用提供的@types/react 包中的FC ,也是一样的。

type WithChildren<T = {}> = T & { children?: React.ReactNode };

type CardProps = WithChildren<{
  title: string;
}>;

function Card(props: CardProps) {
  return (
    <div className="card">
      <h2>{props.title}</h2>
      {props.children}
    </div>
  );
};

到目前为止,一切都很好。现在让我们用一些React节点来使用这个组件。

export default function App() {
  return (
    <div className="App">
      <Card title="Yo!">
        <p>Whats up</p>
      </Card>
    </div>
  );
}

编译。渲染!很好。现在让我们用一个任意的、随机的对象来使用它。

export default function App() {
  const randomObject = {};
  return (
    <div className="App">
      <Card title="Yo!">{randomObject}</Card>
    </div>
  );
}

这也能编译,TypeScript完全没有抛出一个错误。但这是你从浏览器中得到的东西。

Error

Objects are not valid as a React child (found: object with keys {}). 
If you meant to render a collection of children, use an array instead.

哦,不!TypeScript的世界观与我们从库中实际得到的东西不同。这很糟糕。这真的很糟糕。这就是TypeScript应该检查的情况。那么发生了什么?

罪魁祸首#

Definitely Typed的React类型中,有一行几乎完全禁用了对子类型的检查。它目前在第236行,看起来像这样。

interface ReactNodeArray extends Array<ReactNode> {}
type ReactFragment = {} | ReactNodeArray;
type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

通过定义ReactFragment ,允许{} ,我们基本上允许传递任何对象(除了nullundefined ,但请看下一行!)。由于TypeScript是结构化类型的,你可以传入所有空对象的子类型。在JavaScript中,这就是一切

问题是。这不是一个新的变化,它几乎已经存在了很长时间。它是在2015年3月引入的,没有人知道为什么。我们也不知道那时的语义是否会有所不同。

许多人指出了这一点,一些人试图修复它

但由于它已经存在了6年多,这个小小的变化打破了一大批直接连接到React类型的包。这是一个巨大的变化,真的很难处理!这也是一个巨大的变化。所以说实话,我不确定我们是否合理地可以更新这一行。更糟糕的是。所有这些包都有错误的测试和类型。我不知道该如何看待这个问题。

我们能做什么呢#

但是我们总是可以自己定义我们的子类型。如果你使用WithChildren ,那就更容易了。让我们来创建我们自己的ReactNode。

import type { ReactChild, ReactPortal, ReactNodeArray } from "react";

type ReactNode =
  | ReactChild
  | ReactNodeArray
  | ReadonlyArray<ReactNode>
  | ReactPortal
  | boolean
  | null
  | undefined;

type WithChildren<T = {}> = T & { children?: ReactNode };

type CardProps = WithChildren<{
  title: string;
}>;

有了这个,我们就能得到我们想要的错误。

export default function App() {
  const randomObject = {};
  return (
    <div className="App">
      <Card title="Yo!">{randomObject}</Card>{/* 💥 BOOM! */}
    </div>
  );
}

而TypeScript又与现实世界接轨了。

如果你提供一些组件给别人,这就特别有用了!例如,当一些后端数据从简单的字符串变为复杂的对象时,你就能一次性发现代码库中的所有问题,而不是在运行时通过应用程序的崩溃来发现。

注意事项#

如果你是在自己的代码库中,这个方法非常有效。当你需要将你的安全组件与其他组件(例如:使用React.ReactNodeFC<T> )结合起来时,你可能会再次遇到错误,因为类型不匹配。我还没有遇到过这种情况,但永远不要说永远。