我目前和几个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 ,允许{} ,我们基本上允许传递任何对象(除了null 或undefined ,但请看下一行!)。由于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.ReactNode 或FC<T> )结合起来时,你可能会再次遇到错误,因为类型不匹配。我还没有遇到过这种情况,但永远不要说永远。