适用于 React 的极简 TypeScript 速成课程 - 包含交互式代码练习
即使你还没有使用过 TypeScript,你可能也听说过它。过去几年,它在 React 领域得到了广泛的应用。目前,几乎所有 React 工作似乎都需要 TypeScript 知识。
因此,许多 React 开发人员问自己:我真的需要学习 TypeScript 吗?
我明白,你已经有很多事情要做了。尤其是如果你还在努力进入这个行业,你可能会被各种需要学习的东西压得喘不过气来。但请听听这位开发者的分享:
学习 TypeScript 无疑是最好的投资:
- 您获得工作机会会增加。
- 您的代码将会更少错误并且更易于阅读/维护。
- 重构代码和更新依赖项将变得更加容易。
简而言之,你的开发经验将会飞速提升。不过,一开始你可能会遇到一些困难。各种奇怪的错误,以及像被绑在背后(还有剩下的四根手指被粘在一起)一样的编码。
本页面旨在帮助您快速上手,避免繁琐的细节。您将获得关于如何在 React 中使用 TypeScript 的简要介绍。为了巩固所学知识,几乎每个章节后都提供了交互式代码练习。
由于这个话题很容易变得枯燥,所以我们大致跟随这个故事的反英雄:帕特,一个讨厌的首席技术官。
请注意,本页练习使用的代码编辑器比较新。如果您遇到问题,请发送错误报告至bugs@profy.dev。
React 所需的 TypeScript 基础知识
原语
它们的名字正在逐渐消失,但这三个原始类型却是所有类型的核心。
string // e.g. "Pat"
boolean // e.g. true
number // e.g. 23 or 1.99
数组
数组可以由原语或任何其他类型构建。
number[] // e.g. [1, 2, 3]
string[] // e.g. ["Lisa", "Pat"]
User[] // custom type e.g. [{ name: "Pat" }, { name: "Lisa" }]
对象
对象无处不在,它们可以发挥巨大的作用。就像这个例子一样:
const user = {
firstName: "Pat",
age: 23,
isNice: false,
role: "CTO",
skills: ["CSS", "HTML", "jQuery"]
}
真是个“CTO”!描述此对象的 TypeScript 类型如下所示:
type User = {
firstName: string;
age: number;
isNice: boolean;
role: string,
skills: string[];
}
显然,人类并不只由原始人组成。
type User = {
firstName: string;
...
friends: User[];
}
但看看我们的“CTO”,我们可以把字段设为可选,这很好。Pat 显然选择了事业而不是朋友:
const user = {
firstName: "Pat",
age: 23,
isNice: false,
role: "CTO",
skills: ["CSS", "HTML", "jQuery"],
friends: undefined
}
尽管这对 Pat 来说可能很困难,但在 TypeScript 中,这就像添加以下内容一样简单?
:
type User = {
firstName: string;
...
friends?: User[];
}
枚举
请记住,我们将User.role
字段定义为string
。
type User = {
...
role: string,
}
作为“首席技术官”的帕特对此很不高兴。他知道这种string
类型的限制性不够。他的员工不应该能够随意选择他们想要的角色。
枚举来拯救他!
enum UserRole {
CEO,
CTO,
SUBORDINATE,
}
这下好多了!不过 Pat 也不傻:他知道这个枚举值在内部只是数字。虽然 CEO 位居最高(拍马屁的 Pat),但它的数值是0
。零!CTO 是 1。下属是 2?
好像不太合适。怎么每个人都比领导更有价值呢?
幸运的是,我们可以使用字符串值作为枚举。
enum UserRole {
CEO = "ceo",
CTO = "cto",
SUBORDINATE = "inferior-person",
}
当你想传达信息的时候,这非常有用(Pat,我指的是你)。但处理来自 API 的字符串时,它也很有用。
不管怎样,帕特现在很高兴了。他可以放心地给每个人分配合适的角色了。
enum UserRole {
CEO = "ceo",
CTO = "cto",
SUBORDINATE = "inferior-person",
}
type User = {
firstName: string;
age: number;
isNice: boolean;
role: UserRole,
skills: string[];
friends?: User[];
}
const user = {
firstName: "Pat",
age: 23,
isNice: false,
role: UserRole.CTO, // equals "cto"
skills: ["CSS", "HTML", "jQuery"]
}
功能
掌权者最喜欢做什么?我不确定,但帕特肯定喜欢通过解雇一群失败者来炫耀自己的权力。
因此,让我们让 Pat 高兴并编写一个可以提高他的射击性能的函数。
函数参数的类型
我们有三种方法来识别被解雇的人。首先,我们可以使用多个参数。
function fireUser(firstName: string, age: number, isNice: boolean) {
...
}
// alternatively as an arrow function
const fireUser = (firstName: string, age: number, isNice: boolean) => {
...
}
其次,我们可以将所有上述参数包装在一个对象中并以内联方式定义类型。
function fireUser({ firstName, age, isNice }: {
firstName: string;
age: number;
isNice: boolean;
}) {
...
}
最后(由于上面的代码可读性不高),我们还可以提取类型。剧透警告:这是我们在 React 组件 props 中经常看到的做法。
type User = {
firstName: string;
age: number;
role: UserRole;
}
function fireUser({ firstName, age, role }: User) {
...
}
// alternatively as arrow function
const fireUser = ({ firstName, age, role }: User) => {
...
}
函数返回值的类型
简单地解雇一个用户可能对 Pat 来说还不够。也许他想进一步侮辱他们?所以,从函数中返回用户可能是个好主意。
同样,定义返回类型也有多种方法。首先,我们可以: MyType
在参数列表的右括号后面添加。
function fireUser(firstName: string, age: number, role: UserRole): User {
// some logic to fire that loser ...
return { firstName, age, role };
}
// alternatively as an arrow function
const fireUser = (firstName: string, age: number, role: UserRole): User => {
...
}
这强制返回正确的类型。如果我们尝试返回其他类型(例如null
),TypeScript 不会允许我们这么做。
如果我们不想那么严格,我们也可以让 TypeScript 推断返回类型。
function fireUser(user: User) {
// some logic to fire that loser ...
return user;
}
这里我们只是返回输入参数。这也可以作为“触发”逻辑的返回值。但由于返回值的类型user
很明确,TypeScript 会自动识别函数的返回类型。
所以,使用 TypeScript 时,越少越好。你不需要到处定义类型,通常可以依赖类型推断。如有疑问,只需将鼠标光标悬停在相关变量或函数上即可,如上图所示。
你可能会遇到的事情
这里还有一些我们未曾提及但您很快就会遇到的事情。
使用 TypeScript 进行 React
对于 React 和 TypeScript 来说,以上章节的基础知识通常就足够了。毕竟,React 函数组件和 hooks 都是简单的函数。而 props 只是对象。在大多数情况下,你甚至不需要定义任何类型,因为 TypeScript 知道如何推断它们。
使用 TypeScript 的函数组件
大多数情况下,你不需要定义组件的返回类型。你肯定会指定 props 的类型(我们稍后会看到)。但是,一个简单的无 props 组件不需要任何类型。
function UserProfile() {
return <div>If you're Pat: YOU'RE AWESOME!!</div>
}
返回类型JSX.Element
正如您在该屏幕截图中所看到的。
很棒的事情是:如果我们搞砸了并从组件返回任何无效的 JSX 内容,TypeScript 会警告我们。
在这种情况下,user
对象不是有效的 JSX,因此我们会收到错误:
'UserProfile' cannot be used as a JSX component.
Its return type 'User' is not a valid JSX element.
使用 TypeScript 的 Props
这番话UserProfile
确实夸奖了我们的“CTO”Pat。但它却向所有用户传达了同样的信息,感觉像是在侮辱他。显然,我们需要一些鼓励。
enum UserRole {
CEO = "ceo",
CTO = "cto",
SUBORDINATE = "inferior-person",
}
type UserProfileProps = {
firstName: string;
role: UserRole;
}
function UserProfile({ firstName, role }: UserProfileProps) {
if (role === UserRole.CTO) {
return <div>Hey Pat, you're AWESOME!!</div>
}
return <div>Hi {firstName}, you suck!</div>
}
如果您更喜欢箭头函数,相同的组件看起来像这样。
const UserProfile = ({ firstName, role }: UserProfileProps) => {
if (role === UserRole.CTO) {
return <div>Hey Pat, you're AWESOME!!</div>
}
return <div>Hi {firstName}, you suck!</div>
}
最后说明一下:在实际中,你会发现很多代码使用React.FC
或React.FunctionComponent
来输入组件。不过现在不再推荐这样做了。
// using React.FC is not recommended
const UserProfile: React.FC<UserProfileProps>({ firstName, role }) {
...
}
回调属性
众所周知,在 React 中我们经常将回调函数作为 props 传递。目前为止我们还没有见过 function 类型的字段。所以让我快速展示一下:
type UserProfileProps = {
id: string;
firstName: string;
role: UserRole;
fireUser: (id: string) => void;
};
function UserProfile({ id, firstName, role, fireUser }: UserProfileProps) {
if (role === UserRole.CTO) {
return <div>Hey Pat, you're AWESOME!!</div>;
}
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={() => fireUser(id)}>Fire this loser!</button>
</>
);
}
注意:void
是函数的返回类型,代表“无”。
默认道具
记住:我们可以用 标记一个字段,使其成为可选字段?
。我们也可以对可选的 props 执行相同的操作。这里的role
不是必需的。
type UserProfileProps = {
age: number;
role?: UserRole;
}
如果我们想要为可选道具设置默认值,我们可以在重组道具时分配它。
function UserProfile({ firstName, role = UserRole.SUBORDINATE }: UserProfileProps) {
if (role === UserRole.CTO) {
return <div>Hey Pat, you're AWESOME!!</div>
}
return <div>Hi {firstName}, you suck!</div>
}
使用 TypeScript 的 useState Hook(推断类型)
React 中最常用的 hook 是useState()
。很多情况下,你不需要指定它的类型。如果你使用初始值,TypeScript 可以推断出它的类型。
function UserProfile({ firstName, role }: UserProfileProps) {
const [isFired, setIsFired] = useState(false);
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={() => setIsFired(!isFired)}>
{isFired ? "Oops, hire them back!" : "Fire this loser!"}
</button>
</>
);
}
现在我们安全了。如果我们尝试使用布尔值以外的任何其他值来更新状态,就会出现错误。
使用 TypeScript 的 useState Hook(手动输入)
不过,在其他情况下,TypeScript 无法根据初始值正确推断类型。例如:
// TypeScript doesn't know what type the array elements should have
const [names, setNames] = useState([]);
// The initial value is undefined so TS doesn't know its actual type
const [user, setUser] = useState();
// Same story when we use null as initial value
const user = useState(null);
useState()
是用所谓的泛型类型实现的。我们可以用它来正确地定义状态的类型:
// the type of names is string[]
const [names, setNames] = useState<string[]>([]);
setNames(["Pat", "Lisa"]);
// the type of user is User | undefined (we can either set a user or undefined)
const [user, setUser] = useState<User>();
setUser({ firstName: "Pat", age: 23, role: UserRole.CTO });
setUser(undefined);
// the type of user is User | null (we can either set a user or null)
const [user, setUser] = useState<User | null>(null);
setUser({ firstName: "Pat", age: 23, role: UserRole.CTO });
setUser(null);
这应该足够你使用 了useState()
。由于我们专注于 React 的最低 TypeScript 技能,所以这里就不讨论其他钩子了。只需注意useEffect()
不需要输入。
使用 TypeScript 自定义 Hooks
自定义钩子同样只是一个函数。所以你已经知道如何定义它了。
function useFireUser(firstName: string) {
const [isFired, setIsFired] = useState(false);
const hireAndFire = () => setIsFired(!isFired);
return {
text: isFired ? `Oops, hire ${firstName} back!` : "Fire this loser!",
hireAndFire,
};
}
function UserProfile({ firstName, role }: UserProfileProps) {
const { text, hireAndFire } = useFireUser(firstName);
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={hireAndFire}>
{text}
</button>
</>
);
}
使用 TypeScript 来响应事件
使用内联点击处理程序非常简单,因为 TypeScript 已经知道事件参数的类型。您无需手动输入任何内容。
function FireButton() {
return (
<button onClick={(event) => event.preventDefault()}>
Fire this loser!
</button>
);
}
但是,当您创建单独的点击处理程序时,它会变得更加复杂。
function FireButton() {
const onClick = (event: ???) => {
event.preventDefault();
};
return (
<button onClick={onClick}>
Fire this loser!
</button>
);
}
这里的类型是什么event
?这里有两种方法:
- 在 Google 上搜索(不推荐,会让你头晕)。
- 假设您有一个内联函数,并让 TypeScript 为您提供该类型。
复制粘贴愉快!你甚至不需要理解这里发生了什么(提示:这些都是泛型,就像我们在 中看到的那样useState
)。
function FireButton() {
const onClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
};
return (
<button onClick={onClick}>
Fire this loser!
</button>
);
}
那么输入的变更处理程序呢?同样的策略可以揭示:
function Input() {
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};
return <input onChange={onChange} />;
}
还有选择吗?
function Select() {
const onChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
console.log(event.target.value);
};
return <select onChange={onChange}>...</select>;
}
子类型或组件类型
由于我们都是组件组合的粉丝,所以我们需要知道如何输入常见的children
prop。
type LayoutProps = {
children: React.ReactNode;
};
function Layout({ children }: LayoutProps) {
return <div>{children}</div>;
}
这种类型React.ReactNode
赋予了我们很大的自由度。它基本上允许我们将任何东西作为子对象传递(对象除外)。
如果我们想要更严格一些并且只允许标记,我们可以使用React.ReactElement
或JSX.Element
(基本上相同)。
type LayoutProps = {
children: React.ReactElement; // same as JSX.Element
};
正如您所见,这更加严格:
使用第三方库
添加类型
如今,许多第三方库已经自带了相应的类型。在这种情况下,您无需再安装单独的软件包。
但是许多类型也由 GitHub 上的DefinitelyTyped 仓库维护,并由该@types
组织发布(甚至包括 React 类型)。如果你安装一个没有类型的库,导入语句中会报错。
您可以简单地复制并粘贴突出显示的命令并在终端中执行它。
npm i --save-dev @types/styled-components
大多数情况下,你会很幸运,所需的类型可以通过某种方式获得。尤其是对于一些比较流行的库来说。但即使如此,你仍然可以在.d.ts
文件中定义自己的全局类型(不过我们这里就不讲了)。
使用泛型
库通常需要处理大量用例。因此,它们需要灵活。为了定义灵活的类型,需要使用泛型。我们useState
已经见过它们了。这里提醒一下:
const [names, setNames] = useState<string[]>([]);
这在很多第三方库中很常见,这里以 Axios 为例:
import axios from "axios"
async function fetchUser() {
const response = await axios.get<User>("https://example.com/api/user");
return response.data;
}
或者 react-query:
import { useQuery } from "@tanstack/react-query";
function UserProfile() {
// generic types for data and error
const { data, error } = useQuery<User, Error>(["user"], () => fetchUser());
if (error) {
return <div>Error: {error.message}</div>;
}
...
}
或者样式组件:
import styled from "styled-components";
// generic type for props
const MenuItem = styled.li<{ isActive: boolean }>`
background: ${(props) => (props.isActive ? "red" : "gray")};
`;
function Menu() {
return (
<ul>
<MenuItem isActive>Menu Item 1</MenuItem>
</ul>
);
}
策略与故障排除
React 和 TypeScript 入门
使用 TypeScript 创建新项目是最简单的选择。我建议创建一个 Vite + React + TypeScript 项目,或者创建一个 Next.js + TypeScript 项目。
// for Vite run this command and select "react-ts"
npm create vite@latest
// for Next.js run
npx create-next-app@latest --ts
这将完全自动地为您设置。
找到合适的类型
我们已经讨论过这一点,但让我在这里重复一遍:当有疑问时(尤其对事件有用)只需开始输入内联函数,然后让 TypeScript 向您显示正确的类型。
如果你不确定有多少个参数可用,也可以这样做。只需写入(...args) =>
,即可将所有参数放入数组中。
检查类型
查看类型上所有可用字段的最简单方法是使用 IDE 的自动完成功能。此处按 CTRL + 空格键(Windows)或 Option + 空格键(Mac)。
但有时你需要更深入地了解。这很简单(但常常令人困惑),只需按 CTRL + 单击(Windows)或 CMD + 单击(Mac)即可转到类型定义。
读取错误消息
刚开始使用 TypeScript 时,一个主要问题是遇到的各种错误。所有可能出错的地方都可能让你抓狂。所以,养成阅读错误信息的习惯是个好主意。我们来看一个例子:
function Input() {
return <input />;
}
function Form() {
return (
<form>
<Input onChange={() => console.log("change")} />
</form>
);
}
你可能已经看到了问题所在。不过,下面是 TypeScript 显示的错误。
有点困惑!这到底是什么意思? type 又是什么IntrinsicAttributes
?在使用库(例如 React 本身)时,你会遇到很多像这样的奇怪类型名称。
我的建议是:暂时忽略它们。
最重要的信息在最后一行:
Property 'onChange' does not exist on type ...
是不是有点熟悉?看一下<Input>
组件的定义:
function Input() {
return <input />;
}
它没有一个名为 的 prop onChange
。这就是 TypeScript 所抱怨的。
这是一个相当简单的例子。但是接下来呢?
const MenuItem = styled.li`
background: "red";
`;
function Menu() {
return <MenuItem isActive>Menu Item</MenuItem>;
}
天哪!这里输出的错误信息量太大了,很容易让人看晕。我最常用的技巧是滚动到消息底部。很多时候,金块就埋在那里。
function UserProfile() {
const { data, error } = useQuery(
["user"],
{
cacheTime: 100000,
},
() => fetchUser()
);
}
对象作为更清洁类型的道具
在上面的例子中,用户数据通常来自 API。假设我们在User
组件外部定义了类型
export type User = {
firstName: string;
role: UserRole;
}
UserProfile
此刻的组件正是以这个对象作为道具。
function UserProfile({ firstName, role }: User) {
...
}
目前看来这似乎合理。事实上,当我们有现成的用户对象时,渲染这个组件相当容易。
function UserPage() {
const user = useFetchUser();
return <UserProfile {...user} />;
}
但是,一旦我们想要添加User
类型中没有的额外 props,事情就变得困难了。还记得fireUserLOL
上面的函数吗?让我们来使用它。
function UserProfile({ firstName, role, fireUser }: User) {
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={() => fireUser({ firstName, role })}>
Fire this loser!
</button>
</>
);
}
但由于该fireUser
函数未在User
类型上定义,因此我们会收到错误。
为了创建正确的类型,我们可以使用所谓的交叉类型,即将两种类型与 组合起来&
。这基本上是将两种不同类型的所有字段合并为一种类型。
type User = {
firstName: string;
role: UserRole;
}
// this is called an intersection
// UserProfileProps has all fields of both types
type UserProfileProps = User & {
fireUser: (user: User) => void;
}
function UserProfile({ firstName, role, fireUser }: UserProfileProps) {
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={() => fireUser({ firstName, role })}>
Fire this loser!
</button>
</>
);
}
相比于交叉类型,分离类型通常更简洁。在我们的例子中,我们可以使用用户 prop,而不是直接接受所有用户字段。
type User = {
firstName: string;
role: UserRole;
}
type UserProfileProps = {
user: User;
fireUser: (user: User) => void;
}
function UserProfile({ user, onClick }: UserProfileProps) {
return (
<>
<div>Hi {user.firstName}, you suck!</div>
<button onClick={() => fireUser(user)}>
Fire this loser!
</button>
</>
);
}