[译]<<Effective TypeScript>> 技巧32 优先选择 interface 的并集,而不是并集的interface

166 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 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
  }
}