一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第20天,点击查看活动详情。
本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
技巧28: 始终选择表示有效状态的类型
如果你的 types 设计的很糟糕, 那么拿的代码不仅不易阅读, 而且容易出现bug。一个高效 types 的设计原则:让你的 types 永远只表示有效状态。
假定你正在建立一个网站,你可以选择并且加载一个网页,你可能会 写一个 state 像这样:
interface State {
pageText: string;
isLoading: boolean;
error?: string;
}
你在写渲染网页代码时候,会考虑所有情况:
unction renderPage(state: State) {
if (state.error) {
return `Error! Unable to load ${currentPage}: ${state.error}`;
} else if (state.isLoading) {
return `Loading ${currentPage}...`;
}
return `<h1>${currentPage}</h1>\n${state.pageText}`;
}
如果 isLoading 和 error 被同时设置了? 那我们是不是应该同时显示 loading 消息和 error 消息?
另外如果你写一个 changePage 函数:
async function changePage(state: State, newPage: string) {
state.isLoading = true;
try {
const response =
await fetch(getUrlForPage(newPage));
if (!response.ok) {
throw new Error(`Unable to load ${newPage}: ${response.statusText}`);
}
const text = await response.text();
state.isLoading = false;
state.pageText = text;
} catch (e) {
state.error = '' + e;
}
}
这段代码有很多问题:
- 在 error 分支,我们忘记设置 state.isLoading 为 false
- 在前一个请求失败,我们没有对 state.error 进行清空,那么导致加载新页面,还会报错
- 当一个网页正在加载中,用户点击加载另一个网页,他们可能会看到一个新的网页,和之前网页的报错信息。
错误的原因:state的设计有问题:
- state信息太少:是哪个个网页请求失败? 哪个网页正在loading?
- state信息太冗余:当 isLoading , error被同时设置时,这表示一个非法的 state! 这让 render ,changePage 函数无法实现
这有一个 state 更好的实现:
interface RequestPending {
state: 'pending';
}
interface RequestError {
state: 'error';
error: string;
}
interface RequestSuccess {
state: 'ok';
pageText: string;
}
type RequestState = RequestPending | RequestError | RequestSuccess;
interface State {
currentPage: string;
requests: {[page: string]: RequestState};
}
这里运用了 tagged union 技术(可以称作discriminated union). 对网络请求可能处于的不同状态进行显式建模。这个版本的 state 占用内存会更多, 但是有一个非常大的好处:不包含非法state!所以 renderPage 和 changePage非常好实现:
function renderPage(state: State) {
const {currentPage} = state;
const requestState = state.requests[currentPage];
switch (requestState.state) {
case 'pending':
return `Loading ${currentPage}...`;
case 'error':
return `Error! Unable to load ${currentPage}: ${requestState.error}`;
case 'ok':
return `<h1>${currentPage}</h1>\n${requestState.pageText}`;
}
}
async function changePage(state: State, newPage: string) {
state.requests[newPage] = {state: 'pending'};
state.currentPage = newPage;
try {
const response = await fetch(getUrlForPage(newPage));
if (!response.ok) {
throw new Error(`Unable to load ${newPage}: ${response.statusText}`);
}
const pageText = await response.text();
state.requests[newPage] = {state: 'ok', pageText};
} catch (e) {
state.requests[newPage] = {state: 'error', error: '' + e};
}
}
每个网页对应的 state 非常明确!
再来看一个非常简单但是可怕的例子:法航447航班,这架空客330在2009年6月1日在大西洋失踪。空中客车是一架电传飞机,驾驶员所有的控制都会输入计算机系统,然后输出控制飞机姿态。
空客330的驾驶舱为驾驶员和副驾驶配备了一套单独的控制装置。“侧杆”控制着攻角。向后拉会使飞机爬升,向前推会使飞机俯冲。空客330使用了一种称为“双输入”模式的系统,该模式允许两侧杆独立移动。以下是如何在TypeScript中对其状态进行建模:
interface CockpitControls {
/** Angle of the left side stick in degrees, 0 = neutral, + = forward */
leftSideStick: number;
/** Angle of the right side stick in degrees, 0 = neutral, + = forward */
rightSideStick: number;
}
你需要写个 getStickSetting 函数获取操纵杆的状态:
function getStickSetting(controls: CockpitControls) {
return controls.leftSideStick;
}
上面的代码仅仅获取了驾驶员操纵杆的状态,那副驾驶的操纵杆的状态呢?
function getStickSetting(controls: CockpitControls) {
const {leftSideStick, rightSideStick} = controls;
if (leftSideStick === 0) {
return rightSideStick;
}
return leftSideStick;
}
这还有一个问题:如果驾驶员和副驾驶员的操纵杆的状态都为非零呢?, 有一个办法取平均:
function getStickSetting(controls: CockpitControls) {
const {leftSideStick, rightSideStick} = controls;
if (leftSideStick === 0) {
return rightSideStick;
} else if (rightSideStick === 0) {
return leftSideStick;
}
if (Math.abs(leftSideStick - rightSideStick) < 5) {
return (leftSideStick + rightSideStick) / 2;
}
// ???
}
这导致了悲剧的发生:当法航447进入风暴后:
- 副驾驶员偷偷将操纵杆拉到最后。
- 飞机抬头,开始爬升高度的同时进入了失速状态。
- 驾驶员发现飞机失速,立即将操纵杆推倒最前,想让飞机重新获得速度。
- 但是副驾驶员的操纵杆依旧在最后没有被发现,这是当时法航获取操纵杆装动态的函数:
function getStickSetting(controls: CockpitControls) { return (controls.leftSideStick + controls.rightSideStick) / 2; } - 两个操纵杆状态取平均:操纵杆状态为0!最终飞机坠毁!
现如今飞机两个操纵杆状态相关联,如果副驾驶员的操纵杆往后拉,驾驶员的操纵杆将会保持同步。用 types 来表示:
interface CockpitControls {
/** Angle of the stick in degrees, 0 = neutral, + = forward */
stickAngle: number;
}
所以当你设计你的 types 应该考虑应该包含哪些 values ?应该排除哪些 values ?一定要排除非法的stause!