TL;DR

  • ユニオン型のプロパティを複数持つインタフェースは、プロパティ間の関係を表せていないことがある。その場合はインタフェースのユニオンにする。
  • 制御フロー解析で絞り込みやすくするため、タグ付きユニオンを使うとよい。
  • 一緒に存在する複数のオプションプロパティは、1 つのオプションプロパティにまとめる。

内容

インタフェースのユニオンにする

ユニオン型のプロパティを複数持つインタフェースは、プロパティ間の関係を明確に表せていないことがある(項目29)。次の Layer では、layout が FillLayout なのに paint が LinePaint という組み合わせも許してしまう。

// NG
interface Layer {
  layout: FillLayout | LineLayout | PointLayout
  paint: FillPaint | LinePaint | PointPaint
}

この場合は、代わりにインタフェースのユニオンを検討する。

// OK
interface FillLayer {
  layout: FillLayout
  paint: FillPaint
}
interface LineLayer {
  layout: LineLayout
  paint: LinePaint
}
interface PointLayer {
  layout: PointLayout
  paint: PointPaint
}
type Layer = FillLayer | LineLayer | PointLayer

タグ付きユニオンを使う

制御フロー解析で型を絞り込みやすくするため、タグ付きユニオンを使うとよい。このパターンは TypeScript で頻繁に使われる。

interface FillLayer {
  type: "fill"
  layout: FillLayout
  paint: FillPaint
}
interface LineLayer {
  type: "line"
  layout: LineLayout
  paint: LinePaint
}
interface PointLayer {
  type: "point"
  layout: PointLayout
  paint: PointPaint
}
type Layer = FillLayer | LineLayer | PointLayer
const drawLayer = (layer: Layer) => {
  if (layer.type === "fill") {
    const { paint } = layer
    //      ^? const paint: FillPaint
    const { layout } = layer
    //      ^? const layout: FillLayout
  }
  // ...
}

オプションプロパティをまとめる

データを正確にモデリングするため、複数のオプションプロパティを 1 つにまとめられないか検討する。次の Person では、placeOfBirth と dateOfBirth は両方存在するか、両方存在しないかのどちらかだが、型からはそれが分からない。

// NG
interface Person {
  name: string
  placeOfBirth?: string
  dateOfBirth?: Date
}
// OK
interface Person {
  name: string
  birth?: {
    placeOfBirth: string
    dateOfBirth: Date
  }
}

データ構造を変えられない場合は、インタフェースのユニオンを使えばよい。

interface Name {
  name: string
}
interface PersonWithBirth extends Name {
  placeOfBirth: string
  dateOfBirth: Date
}
type Person = Name | PersonWithBirth

キーワード