作为一个前端开发者,你的工作不仅仅是移动像素,前端的大部分复杂性来自于处理你的应用程序可能处于的所有不同状态。
它可能是加载数据,等待表单被填写,或者发送一个遥测事件 - 或者同时进行这三项。
如果不能正确处理状态,就可能陷入困境,而处理状态要从它们在类型中如何表示开始。
“可选包”
假设你正在构建一个简单的数据加载器,你可以选择使用如下类型来表示其状态:
interface State {
status: "loading" | "error" | "success";
error?: Error;
data?: { id: string };
}
// Some examples:
const example: State = {
status: "loading"
};
const example2: State = {
status: "error",
error: new Error("Oh no!")
};
这看起来很不错 - 我们可以检查 status
来理解我们应该在屏幕上显示哪种 UI。
除了 - 这种类型让我们声明所有应该不可能的形状:
const example3: State = {
status: "success",
// Where's the data?!
};
这里,我们处于成功状态 - 这应该让我们访问数据。但它不存在!
const example4: State = {
status: "loading",
// We're loading, but we still have an error?!
error: new Error("Eek!"),
};
在这里,我们处于加载状态 - 但我们的数据对象仍然有一个错误!
这是因为我们选择使用我称之为“可选包”的状态来表示我们的状态——一个充满可选属性的对象。
可选属性最好用于值可能存在或可能不存在的情况,在本例中,这是不正确的。
-
当
status
是loading
时,data
或error
永远不存在。 -
当
status
是success
时,data
总是存在。 -
当
status
是error
时,error
总是存在。
可区分联合
更准确地表示这种情况的方法是使用 discriminated union,即可区分联合。
让我们从将状态更改为对象的联合开始,每个对象都包含一个状态。
type State =
| {
status: "loading";
}
| {
status: "success";
}
| {
status: "error";
};
现在我们已经有了基本框架,可以开始向联合的每个分支添加元素了。让我们重新添加错误和数据类型。
type State =
| {
status: "loading";
}
| {
status: "success";
data: {
id: string;
};
}
| {
status: "error";
error: Error;
};
现在,上面的例子将开始出错。
// Error: Property 'data' is missing
const example3: State = {
status: "success",
};
const example4: State = {
status: "loading",
// Error: Object literal may only specify known
// properties, and 'error' does not exist
error: new Error("Eek!"),
};
我们的 State
类型现在正确地表示了特性的所有可能状态,这是向前迈出的一大步,但我们还没有完成。
解构可区分联合
让我们假设我们位于代码库中的组件内部,我们已经接收到状态片段,并希望使用它来渲染一些 JSX。
我在这里使用 React,但这可以是任何前端框架。
许多开发人员的第一反应是解构 State
的元素,但您会立即遇到错误:
const Component = () => {
const [state, setState] = useState<State>({
status: "loading",
});
const {
status,
// Error: Property 'data' does not exist on type 'State'.
data,
// Error: Property 'error' does not exist on type 'State'.
error,
} = state;
};
对于许多开发者来说,这将是棘手的问题,因为 data
和 error
都可以存在于 State
上,所以为什么我得到错误?
原因是我们还没有试图区分这个 union,我们不知道自己在哪个状态,所以唯一可用的属性是 union 中所有成员共享的属性,即 status
。
一旦我们检查了我们所在的联合分支,我们就可以安全地解构 state
!
if (state.status === "success") {
const { data } = state;
}
这种严格性是一个特性,而不是一个 bug,通过确保你只能在状态等于 success
时访问数据,鼓励你从状态的角度来考虑应用,并且只在可用的状态下访问数据。
总结
当你开始从区分状态的角度考虑你的应用时,很多事情就变得容易了。
你将开始理解数据和 UI 之间的联系,而不是一个可选的大数据包。
不仅如此,你还能以一种全新的方式思考属性。
如果你需要以两种略有不同的方式显示组件,可以使用 discriminated union:
type ModalProps =
| {
variant: "with-description-and-button";
buttonText: string;
description: string;
title: string;
}
| {
variant: "base";
title: string;
};
这里,只有当传入的变量是 with-description-and-button
时,才需要 buttonText
和 description
。
欢迎关注公众号:文本魔术,了解更多