一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第24天,点击查看活动详情。
本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
技巧32:优先选择 interface 的并集,而不是并集的interface
假设您正在构建一个矢量绘图程序,并定义了一个接口:
interface Layer {
layout: FillLayout | LineLayout | PointLayout;
paint: FillPaint | LinePaint | PointPaint;
}
layout控制形状,paint控制样式。但是这存在一个问题:如果layout是 FillLayout类型,而 paint 是LinePaint类型。这是非法的值。
有一个更好的实现办法:
interface FillLayer {
layout: FillLayout;
paint: FillPaint;
}
interface LineLayer {
layout: LineLayout;
paint: LinePaint;
}
interface PointLayer {
layout: PointLayout;
paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;
这样定义的好处:避免了上文提到的错误,避免了不同字段组合导致的非法的状态。
这种模式被称为tagged union (or “discriminated union”).在本例子中,还有一个字段:type:
interface Layer {
type: 'fill' | 'line' | 'point';
layout: FillLayout | LineLayout | PointLayout;
paint: FillPaint | LinePaint | PointPaint;
}
将其转换为tagged union:
interface FillLayer {
type: 'fill';
layout: FillLayout;
paint: FillPaint;
}
interface LineLayer {
type: 'line';
layout: LineLayout;
paint: LinePaint;
}
interface PointLayer {
type: 'paint';
layout: PointLayout;
paint: PointPaint;
}
type Layer = FillLayer | LineLayer | PointLayer;
type字段就是 「tag」,用来在js运行时判断 Layer 的类型,同时可以用来 ts 编译器缩小 Layer 类型:
function drawLayer(layer: Layer) {
if (layer.type === 'fill') {
const {paint} = layer; // Type is FillPaint
const {layout} = layer; // Type is FillLayout
} else if (layer.type === 'line') {
const {paint} = layer; // Type is LinePaint
const {layout} = layer; // Type is LineLayout
} else {
const {paint} = layer; // Type is PointPaint
const {layout} = layer; // Type is PointLayout
}
}
通过这样精准的建模,帮助 ts 正确检查你的代码。同时这里慎用类型断言。我们应该尽可能使用 tagged union模式。
同时可选参数也可用于 tagged union 模式,例如有这样的interface:
interface Person {
name: string;
// These will either both be present or not be present
placeOfBirth?: string;
dateOfBirth?: Date;
}
有一个问题:这里的 placeOfBirth 字段和 dateOfBirth 字段应该同时存在,或者同时不存在。所以更好的实现:
interface Person {
name: string;
birth?: {
place: string;
date: Date;
}
}
所以当placeOfBirth 字段和 dateOfBirth 字段有一个不存在时候,就会报错:
const alanT: Person = {
name: 'Alan Turing',
birth: {
// ~~~~ Property 'date' is missing in type
// '{ place: string; }' but required in type
// '{ place: string; date: Date; }'
place: 'London'
}
}
这样当我们获取birth字段的value,我们只需要一次间就就好:
function eulogize(p: Person) {
console.log(p.name);
const {birth} = p;
if (birth) {
console.log(`was born on ${birth.date} in ${birth.place}.`);
}
}
当当你无法直接定义上面的type,你可以使用tagged union来定义:
interface Name {
name: string;
}
interface PersonWithBirth extends Name {
placeOfBirth: string;
dateOfBirth: Date;
}
type Person = Name | PersonWithBirth;
同样的,也只需要一次检查:
function eulogize(p: Person) {
if ('placeOfBirth' in p) {
p // Type is PersonWithBirth
const {dateOfBirth} = p // OK, type is Date
}
}