TypeScript 最强大的特性之一就是就是它提供了一种建模方式,能够去区分处于多种不同状态的类型,这种判别模式就叫作 Discriminated Unions。
先来看一个不好的设计
比如我们通过 fetch
获取数据,那么就可能会存在三种状态:请求中/请求成功/请求失败, 我们可以通过一个状态字段去表示
type State = {
status: "loading" | "success" | "error";
};
看起来很完美,并没有什么问题,但是除了状态信息,我们还需要更多的内容,比如如果请求成功,我们需要拿到请求的数据,如果请求失败,则要拿到失败的信息。
这些信息都是可能存在也可能不存在,所以它们是 Optional Properties 。
type State = {
status: "loading" | "success" | "error";
error?: string;
data?: string;
};
然后我们会有地方去消费这些数据,假设我们把它显示在视图上
const renderUI = (state: State) => {
if (state.status === "loading") {
return "Loading...";
}
if (state.status === "error") {
return `Error: ${state.error.toUpperCase()}`;
// ^^^^^^^^^^^
// ❗ 'state.error' is possibly 'undefined'.
}
if (state.status === "success") {
return `Data: ${state.data}`;
}
};
会发现在 state.error
这边出现了 TS 类型报错,TS 告诉我们,state.error
可能是未定义的,而我们不能在未定义的情况下调用 toUpperCase
。
原因是因为我们类型定义的有问题,这种类型定义的方式导致 error
和 data
是否存在是和 status
的状态无关,我们甚至可以创建一个在工程中根本不可能出现的数据:
type State = {
status: "loading" | "success" | "error";
error?: string;
data?: string;
};
const state: State = {
status: "loading",
error: "This is an error", // should not happen on "loading!"
data: "This is data", // should not happen on "loading!"
};
我们把这种类型描述为 "bag of optionals"。这是一种过于松散的类型。我们需要收紧它,使错误只能发生在错误时,而数据只能发生在成功时。
解决方案就是 Discriminated Unions
解决方案就是把我们的类型转换成 discriminated union,discriminated union 是一种存在共同属性的类型的集合,共同属性的值对于集合的每个成员都是唯一的。让我们把每个状态分成不同的对象字面量:
type State =
| {
status: "loading";
}
| {
status: "error";
}
| {
status: "success";
};
然后,我们可以将错误和数据属性分别与错误和成功状态关联起来:
type State =
| {
status: "loading";
}
| {
status: "error";
error: string;
}
| {
status: "success";
data: string;
};
现在,如果我们将鼠标悬停在 renderUI
函数中的 state.error
,就会发现 TypeScript 知道 state.error
是一个字符串:
这是由于 TS 缩小了范围,它知道 state.status
是 error
,所以它知道 state.error
是 if
代码块内部的字符串。
为了清理原始类型,我们可以为每个状态使用一个类型别名:
type LoadingState = {
status: "loading";
};
type ErrorState = {
status: "error";
error: string;
};
type SuccessState = {
status: "success";
data: string;
};
type State = LoadingState | ErrorState | SuccessState;
因此,如果我们平时发现自己的类型类似于 "bag of optionals",不妨考虑使用 discriminated union。
总结
- discriminated union 是一种为类型建模的方法,这种类型可以处于几种不同的状态之一。
- 它们有助于解决 "bag of optionals" 问题,即由于可选属性过多而导致类型过于松散。
- discriminated union 有一个 "discriminant"–对联合中的每个成员都是唯一的字面类型。
- TypeScript 可以根据判别式属性缩小判别联合的类型。