面向 React 开发人员的高级 TypeScript - 第 3 部分
这是“面向 React 开发者的 TypeScript 高级教程”系列的第三篇文章。在前几章中,我们与雄心勃勃的开发者 Judi 一起,了解了TypeScript 泛型在创建可复用 React 组件方面的作用,并理解了类型保护、keyof、typeof、is、as const 和索引类型等 TypeScript 概念。我们在与 Judi 合作开发亚马逊的竞争对手产品时做到了这一点:一个拥有不同商品类别并支持通过 select 组件进行选择的在线网站。现在是时候再次改进这个系统了,并在此过程中学习详尽性检查的目的是什么、类型缩减是如何工作的以及TypeScript 枚举在什么时候会派上用场。
您可以在codesandbox中看到我们开始的示例的代码。
使用 never 检查彻底性
让我们回顾一下我们是如何实现带有类别的 Tab 的。我们有一个字符串数组,一个switch
为每个 Tab 返回一个 select 组件的 case,以及一个用于类别本身的 select 组件。
const tabs = ["Books", "Movies", "Laptops"] as const;
type Tabs = typeof tabs;
type Tab = Tabs[number];
const getSelect = (tab: Tab) => {
switch (tab) {
case "Books":
return (
<GenericSelect<Book> ... />
);
case "Movies":
return (
<GenericSelect<Movie> ... />
);
case "Laptops":
return (
<GenericSelect<Laptop> ... />
);
}
};
export const TabsComponent = () => {
const [tab, setTab] = useState<Tab>(tabs[0]);
const select = getSelect(tab);
return (
<>
Select category:
<GenericSelect<Tab>
onChange={(value) => setTab(value)}
values={tabs}
formatLabel={formatLabel}
/>
{select}
</>
);
};
所有代码都经过了完美的类型校验,所以如果任何地方出现拼写错误,Typescript 都能识别出来。但是,代码真的完美吗?如果我想在列表中添加一个新类别,会发生什么Phones
?这似乎很简单:我只需将其添加到数组和 switch 语句中即可。
const tabs = ["Books", "Movies", "Laptops", "Phones"] as const;
const getSelect = (tab: Tab) => {
switch (tab) {
// ...
case "Phones":
return (
<GenericSelect<Phone> ... />
);
}
};
在像这样的简单实现中,这不会带来太多麻烦。但在实际应用中,这些代码很可能会被分离、抽象,并隐藏在层层实现之后。如果我只是将 Phones 添加到数组中,而忘记了 switch case,会发生什么?
const tabs = ["Books", "Movies", "Laptops", "Phones"] as const;
const getSelect = (tab: Tab) => {
switch (tab) {
case "Books":
// ...
case "Movies":
// ...
case "Laptops":
// ...
}
};
不幸的是,这种实现方式没什么好处。Typescript 完全可以接受,手动测试时可能会遗漏这个 bug,它会进入生产环境,而且当客户在菜单中选择“电话”时,屏幕上什么也看不到。
但情况并非一定如此。当我们使用if
或 之类的运算符时, switch
TypeScript 会执行所谓的“缩小”操作,即在每个语句中减少联合类型的可用选项。例如,如果我们有一个只包含“Books”的 switch case,那么“Books”类型将在第一个case
语句中被消除,但其余类型将在稍后可用:
const tabs = ["Books", "Movies", "Laptops"] as const;
// Just "Books" in the switch statement
const getSelect = (tab: Tab) => {
switch (tab) {
case "Books":
// tab's type is Books here, it will not be available in the next cases
return <GenericSelect<Book> ... />
default:
// at this point tab can be only "Movies" or "Laptops"
// Books have been eliminated at the previous step
}
};
如果我们使用所有可能的值,typescript 将表示永远不会存在的状态作为never
类型。
const tabs = ["Books", "Movies", "Laptops"] as const;
const getSelect = (tab: Tab) => {
switch (tab) {
case "Books":
// "Books" have been eliminated here
case "Movies":
// "Movies" have been eliminated here
case "Laptops":
// "Laptops" have been eliminated here
default:
// all the values have been eliminated in the previous steps
// this state can never happen
// tab will be `never` type here
}
};
仔细观察手势,你会发现这个技巧:在这种“不可能”的状态下,你可以明确指出 tab 应该是never
type。如果出于某种原因,这并非不可能(例如,我们向数组中添加了“Phones”,但没有添加switch
),TypeScript 就会失败!
// Added "Phones" here, but not in the switch
const tabs = ["Books", "Movies", "Laptops", "Phones"] as const;
// Telling typescript explicitly that we want tab to be "never" type
// When this function is called, it should be "never" and only "never"
const confirmImpossibleState = (tab: never) => {
throw new Error(`Reacing an impossible state because of ${tab}`);
};
const getSelect = (tab: Tab) => {
switch (tab) {
case "Books":
// "Books" have been eliminated
case "Movies":
// "Movies" have been eliminated
case "Laptops":
// "Laptops" have been eliminated
default:
// This should be "impossible" state,
// but we forgot to add "Phones" as one of the cases
// and "tab" can still be the type "Phones" at this stage.
// Fortunately, in this function we assuming tab is always "never" type
// But since we forgot to eliminate Phones, typescript now will fail!
confirmImpossibleState(tab);
}
};
现在实现完美了!任何拼写错误都会被 TypeScript 识别出来,不存在的类别也会被识别出来,甚至遗漏的类别也会被识别出来!顺便说一下,这个技巧叫做“详尽性检查” 。
无需 never 的彻底性检查
有趣的是,要使穷举技巧奏效,你实际上并不需要 never
类型和“不可能”状态。你只需要理解这个缩小和消除的过程,以及如何在最后一步“锁定”所需的类型。
还记得吗,我们有一个formatLabel
传递给选择组件的函数,该函数根据值类型返回选择选项所需的字符串?
export type DataTypes = Book | Movie | Laptop | string;
export const formatLabel = (value: DataTypes) => {
if (isBook(value)) return `${value.title}: ${value.author}`;
if (isMovie(value)) return `${value.title}: ${value.releaseDate}`;
if (isLaptop(value)) return value.model;
return value;
};
另一个完全一样的 bug 的完美候选——如果我们将其添加Phone
为数据类型之一,但忘记了实际的检查,会发生什么?在当前的实现下——同样不会有什么好结果,手机选择选项将会失效。但是,如果我们将详尽的知识应用到函数中,我们可以这样做:
export type DataTypes = Book | Movie | Laptop | Phone | string;
// When this function is called the value should be only string
const valueShouldBeString = (value: string) => value;
const formatLabel = (value: DataTypes) => {
// we're eliminating Book type from the union here
if (isBook(value)) return `${value.title}: ${value.author}`;
// here value can only be Movie, Laptop, Phone or string
// we're eliminating Movie type from the union here
if (isMovie(value)) return `${value.title}: ${value.releaseDate}`;
// here value can only be Laptop, Phone or string
// we're eliminating Laptop type from the union here
if (isLaptop(value)) return value.model;
// here value can only be Phone or string
// But we actually want it to be only string
// And make typescript fail if it is not
// So we just call this function, that explicitly assigns "string" to value
return valueShouldBeString(value);
// Now, if at this step not all possibilities are eliminated
// and value can be something else other than string (like Phone in our case)
// typescript will pick it up and fail!
};
在最后一步,我们排除了除string
、 和“锁定”字符串之外的所有可能的联合类型。是不是很棒?
使用枚举提高代码可读性
现在是时候对这幅精美的 TypeScript 作品——也就是我们的类别实现——进行最后的润色了。我不知道你是怎么想的,但这部分我有点担心:
const tabs = ["Books", "Movies", "Laptops"] as const;
type Tabs = typeof tabs;
type Tab = Tabs[number];
它本身并没有什么问题,只是每次看到这样的结构时,我的脑子都会有点崩溃。我总得额外花一两秒钟才能理解这里到底发生了什么。幸运的是,对于那些遇到同样问题的人来说,有一种方法可以改进它。你知道 TypeScript 支持枚举吗?它们允许定义一组命名常量。最棒的是,它们从一开始就是强类型的,你可以同时使用同一个枚举作为类型和值。🤯
基本上是这样的:
const tabs = ["Books", "Movies", "Laptops"] as const;
type Tabs = typeof tabs;
type Tab = Tabs[number];
可以用这个来代替,可以说它更容易阅读,也更直观:
enum Tabs {
'MOVIES' = 'Movies',
'BOOKS' = 'Books',
'LAPTOPS' = 'Laptops',
}
然后,当您需要访问特定值时,您可以使用点符号,就像对象一样:
const movieTab = Tabs.MOVIES; // movieTab will be `Movies`
const bookTab = Tabs.BOOKS; // bookTab will be `Books`
只需Tabs
在您想要引用枚举作为类型时使用即可!
如果我们查看标签代码,我们可以将所有标签类型替换为枚举标签,将所有标签字符串替换为枚举值:
并且,在Tabs组件的实际实现中也是一样:替换类型,替换值,并将枚举的值以数组的形式传递给选择组件:
完美!😍😎
今天就到这里,希望你喜欢这篇文章,并且对 TypeScript 的收缩、穷举检查和枚举功能更加有信心了。下次再见 😉
...
最初发表于https://www.developerway.com。该网站还有更多类似的文章 😉
订阅时事通讯、在 LinkedIn 上联系或在 Twitter 上关注,以便在下一篇文章发布时立即收到通知。
鏂囩珷鏉ユ簮锛�https://dev.to/adevnadia/advanced-typescript-for-react-developers-part-3-p4j