面向 React 开发人员的 Typescript 泛型
最初发表于https://www.developerway.com。该网站还有更多类似的文章 😉
我不知道你怎么样,但我每次读 TypeScript 文档的时候都会睡着。它的写作方式总在向我的大脑发出信号,让我在睡个好觉、喝上三杯咖啡,最好再来点巧克力来刺激脑细胞之前,根本不应该尝试去理解它。我想我现在找到了接下来几个月的目标:我想以一种普通读者都能理解的方式重写 TypeScript 文档😊
让我们从许多开发人员都在努力解决的一个痛点开始:泛型!我们将采用自下而上的方法:让我们实现一个不使用泛型的组件,并且仅在需要时才引入它们。
简介
隆重推出:Judi 👩🏽💻。Judi 是一位雄心勃勃的开发者,她想建立自己的网店,与亚马逊竞争。她将在那里销售各种商品:书籍、电影以及超过一千种不同类别的商品。现在,她需要实现一个页面,其中包含多个类别的商品,并且每个页面都包含许多外观相同的选项。
她开始非常简单:一个选择组件,它接受一个带有选项的数组value
并title
渲染这些选项,以及一个onChange
处理程序,以便当选择中的值发生变化时她可以做一些事情(每个选择都会做不同的事情!)。
import React from 'react';
type SelectOption = {
value: string;
label: string;
};
type SelectProps = {
options: SelectOption[];
onChange: (value: string) => void;
};
export const Select = ({ options, onChange }: SelectProps) => {
return (
<select onChange={(e) => onChange(e.target.value)}>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);
};
这似乎是一个不错的解决方案:她可以将这些选择重新用于她的所有产品并接管网上购物世界。
<>
<Select option={bookOptions} onChange={(bookId) => doSomethingWithBooks(bookId)} />
<Select option={movieOptions} onChange={(movieId) => doSomethingWithMovies(movieId)} />
</>
不幸的是,随着商店的扩大,她发现这个解决方案存在一些问题:
-
选择组件接受特定格式的选项,消费者组件需要将所有内容转换为该格式。随着商店规模的扩大,越来越多的页面开始使用它,转换代码开始泛滥,变得难以维护。
-
onChange
处理程序仅返回id
已更改值,因此每次她需要找到已更改的实际值时,她都需要手动筛选数据数组 -
它完全不安全,很容易出错。有一次她错误地
doSomethingWithBooks
在 select with 语句中使用了 handlermoviesOptions
,结果搞坏了整个页面,引发了事故。客户很不满意 😞
💪 重构的时间
Judi 希望大幅改进她的申请,并且:
- 删除所有通过原始数据数组进行过滤的代码
- 删除所有生成选择选项的代码
- 使选择组件类型安全,以便下次她使用带有一组选项的错误处理程序时,类型系统可以捕获它
她决定,她需要的是一个精选的组件:
- 接受一组类型值并将其转换为选择选项
onChange
处理程序返回“原始”类型的值,而不仅仅是它的 id,因此无需在消费者端手动搜索它options
并且onChange
值应该是相连的;这样,如果她doSomethingWithBooks
在选择中使用接受电影作为值,那么它将被类型系统捕获。
她已经输入了所有数据,因此只有选择组件需要做一些工作。
export type Book = {
id: string;
title: string;
author: string; // only books have it
};
export type Movie = {
id: string;
title: string;
releaseDate: string; // only movies have it
};
... // all other types for the shop goods
强类型选择 - 第一次尝试
朱迪又从简单的事情开始:她决定先实现一个只接受书籍的选择,然后再修改它以接受其余的类型。
type BookSelectProps = {
values: Book[];
onChange: (value: Book) => void;
};
export const BookSelect = ({ values, onChange }: BookSelectProps) => {
const onSelectChange = (e) => {
const val = values.find((value) => value.id === e.target.value);
if (val) onChange(val);
};
return (
<select onChange={onSelectChange}>
{values.map((value) => (
<option key={value.id} value={value.id}>
{value.title}
</option>
))}
</select>
);
};
这看起来已经很棒了:现在她不需要担心混合处理程序或值,这个选择只接受 Books 作为属性,并且在值发生变化时始终返回一本书。
现在,她需要做的就是把它变成BookSelect
一个函数GenericSelect
,并教它如何处理应用中的其他数据。首先,她尝试对值进行联合类型运算(如果你不熟悉联合类型,它只是一种or
类型的运算符的缩写)。
但她几乎立刻就意识到,这不是一个好主意。这不仅是因为她必须手动列出select 中所有支持的数据类型,并在每次添加新数据类型时进行更改。而且,从代码复杂性的角度来看,这实际上使事情变得更糟:无论 中输入了什么,TypeScript实际上都不知道onChange
这种方法回调中传递的是什么values
。因此,即使是记录所选书籍作者这个最明显、最简单的用例,也会让 TypeScript 非常困惑:
t 知道 value 中可以有Book
或Movie
,但它不知道具体是什么。而且由于Movie
没有 author 字段,TypeScript 会认为上述代码是错误的。
强类型选择-使用 TypeScript 泛型的实际解决方案
这时,TypeScript泛型终于派上用场了。简而言之,泛型只不过是类型的占位符。它告诉 TypeScript:我知道这里会有一个类型,但我还不知道它应该是什么,以后再告诉你。文档中使用的泛型最简单的示例如下:
function identity<Type>(a: Type): Type {
return a;
}
大致翻译过来就是:“我想定义一个函数,它接受某种类型的参数,并返回一个完全相同类型的值。稍后我会告诉你它是什么类型。”
然后在代码的后面,您可以告诉这个函数这个占位符类型的确切含义:
const a = identity<string>("I'm a string") // "a" will be a "string" type
const b = identity<boolean>(false) // "b" will be a "boolean" type
然后任何输错的尝试都会失败:
const a = identity<string>(false) // typescript will error here, "a" can't be boolean
const b = identity<boolean>("I'm a string") // typescript will error here, "b" can't be string
因此将其应用于选择组件的方法是这样的:
现在,我故意不在这里添加可复制粘贴的代码,因为这个例子实际上行不通😅。第一个原因是Typescript 中 React特有的:因为这是一个 React 组件,Typescript 会假设第一个元素<Tvalue>
是一个jsx
元素,从而导致失败。第二个原因是泛型问题:当我们尝试在 select 中访问value.title
或时value.id
,Typescript 此时仍然不知道我们想要将这个值的类型设置为哪种。它不知道我们的值可以拥有哪些属性,这理所当然。为什么会这样呢?
这就引出了这个难题的最后一部分:通用约束。
约束用于缩小泛型类型,以便 TypeScript 至少可以对 做出一些假设TValue
。基本上,这是一种告诉 TypeScript 的方法:我还不知道TValue
应该是什么,但我知道它至少会有和 id
,title
所以你可以自由地假设它们会在那里。
现在,select 组件已经完成,并且功能齐全!💥 🎉 来看看吧:
type Base = {
id: string;
title: string;
};
type GenericSelectProps<TValue> = {
values: TValue[];
onChange: (value: TValue) => void;
};
export const GenericSelect = <TValue extends Base>({ values, onChange }: GenericSelectProps<TValue>) => {
const onSelectChange = (e) => {
const val = values.find((value) => value.id === e.target.value);
if (val) onChange(val);
};
return (
<select onChange={onSelectChange}>
{values.map((value) => (
<option key={value.id} value={value.id}>
{value.title}
</option>
))}
</select>
);
};
最后,Judi 可以用它来实现她想要的亚马逊竞争对手的所有选择:
// This select is a "Book" type, so the value will be "Book" and only "Book"
<GenericSelect<Book> onChange={(value) => console.log(value.author)} values={books} />
// This select is a "Movie" type, so the value will be "Movie" and only "Movie"
<GenericSelect<Movie> onChange={(value) => console.log(value.releaseDate)} values={movies} />
React Hooks 中的 Typescript 泛型奖励
你知道吗?大多数 React hooks 也都是泛型的。你可以显式地输入类似useState
or 的内容useReducer
,这样可以避免复制粘贴驱动的开发错误,避免在开发过程中意外地定义const [book, setBook] = useState();
并传递值。这样的错误可能会导致下一个阅读代码并在重构过程中movie
看到它,从而导致现实崩溃。setBook(movie)
这将正常工作,尽管对于任何试图修复此设置中的错误的人来说,这会引起很多愤怒和绝望:
export const AmazonCloneWithState = () => {
const [book, setBook] = useState();
const [movie, setMovie] = useState();
return (
<>
<GenericSelect<Book> onChange={(value) => setMovie(value)} values={booksValues} />
<GenericSelect<Movie> onChange={(value) => setBook(value)} values={moviesValues} />
</>
);
};
这将阻止它,并且任何在第二个选择中对值使用 setBook 的恶意尝试都将被 typescript 阻止:
export const AmazonCloneWithState = () => {
const [book, setBook] = useState<Book | undefined>(undefined);
const [movie, setMovie] = useState<Movie | undefined>(undefined);
return (
<>
<GenericSelect<Book> onChange={(value) => setBook(value)} values={booksValues} />
<GenericSelect<Movie> onChange={(value) => setMovie(value)} values={moviesValues} />
</>
);
};
今天就到这里,希望你喜欢这篇文章,并且泛型不再是个谜!✌🏼
...
最初发表于https://www.developerway.com。该网站还有更多类似的文章 😉
订阅时事通讯、在 LinkedIn 上联系或在 Twitter 上关注,以便在下一篇文章发布时立即收到通知。
文章来源:https://dev.to/adevnadia/typescript-generics-for-react-developers-4lji