Typescript(判別可能なUnion型)

はじめに

今回は、前回記事と関連して特別な機能である判別可能なUnion型(Descriminated Unions)についての記事になります。この記事の理解が追いつかない方は、以下2つの記事が参考になると思うので是非見てください!
tsuboi99553758.hatenablog.com
tsuboi99553758.hatenablog.com

判別可能なUnion型のユースケース

利用目的は、オブジェクトのunion型を使う時に型ガードを実装することを楽にすることにあります。
前回の型ガードの復習も兼ねて、以下コード上で見ていきましょう。

interface Person {
  name: string;
  age: number;
}
interface Book {
  name: string;
  price: number;
}

function doStuff(arg: Person | Book) {
  console.log(arg.name);
  console.log(arg.age);// エラー(プロパティ 'age' は型 'Book' に存在しません。)

}

簡単な例です。interfaceでオブジェクトの型を定義し、doStuffという関数の引数にユニオン型でインターフェース名を渡しています。返り値は型推論からもvoid型でログを2つ表示させようとしています。当然、arg.ageについてはエラーが発生します。エラー内容から分かるようにBookというインターフェースにはageプロパティが存在しないためです。
そのため前回記事のように、型ガード(in)を使えばこのエラーは解決です。

interface Person {
  name: string;
  age: number;
}
interface Book {
  name: string;
  price: number;
}

function doStuff(arg: Person | Book) {
  if ("age" in arg) {
    console.log(arg.age);
  } else {
    console.log(arg.price);
  }
  console.log(arg.name);
}}

ただしこのケースでinを使った型ガードには2つ問題点があります。
一つの問題は、インターフェースの数が増えればプロパティ名が重複する可能性があるということです。
どういうことか以下のコードを見てください。

interface American {
  name: string;
  age: number;
  greeting: "hello";
}
interface Japanese {
  name: string;
  age: number;
  greeting: "こんにちは";
}
interface Cookbooks {
  name: string;
  price: number;
}
interface Comic {
  name: string;
  price: number;
}
type Person = American | Japanese;
type Book = Cookbooks | Comic;

function doStuff(arg: Person | Book) {
  if ("age" in arg) {
    console.log(arg.age);//AmericaかJapaneseどちらか絞り込めていない。
  } else {
    console.log(arg.price);
  }
  console.log(arg.name);
}

このようにインターフェースの数が増えれば、if文内の"age"プロパティはAmericanとJapaneseのインターフェースで重複しています。これでは型ガードによって条件分岐できたことにはなりません。

もう一つの問題は、if文の中でプロパティ名を過って記述した場合Typescriptはエラーを出してくれません。

interface Person {
  name: string;
  age: number;
}
interface Book {
  name: string;
  price: number;
}

function doStuff(arg: Person | Book) {
  if ("aggge" in arg) {//'age'の間違え(エラーなし)
    console.log(arg.age);//agggeとプロパティは存在しないためここではエラーを出してくれる。
  } else {
    console.log(arg.price);
  }
  console.log(arg.name);
}

以上2つの問題を改善するために、判別可能なUnion型を使いましょう。

interface Person {
  type: "person";
  name: string;
  age: number;
}
interface Book {
  type: "book";
  name: string;
  price: number;
}

function doStuff(arg: Person | Book) {
  if (arg.type === "person") {
    //'age'の間違え
    console.log(arg.age);
  } else {
    console.log(arg.price);
  }
  console.log(arg.name);
}

ポイントは各インターフェースに共通のプロパティ名を持たせることです。
問題となった(インターフェースの数が増えればプロパティ名が重複する可能性がある)で挙げたコードを判別可能なUnion型で修正すると、

interface American {
  type: "american";
  name: string;
  age: number;
  greeting: "hello";
}
interface Japanese {
  type: "japanese";
  name: string;
  age: number;
  greeting: "こんにちは";
}
interface Cookbooks {
  type: "cookbooks";
  name: string;
  price: number;
}
interface Comic {
  type: "comic";
  name: string;
  price: number;
}
type Person = American | Japanese;
type Book = Cookbooks | Comic;

function doStuff(arg: Person | Book) {
  if (arg.type === "american") {
    console.log(arg.age);
  }
  if (arg.type === "japanese") {
    //Japanese
  }
  if (arg.type === "comic") {
    //Comic
  }
  if (arg.type === "cookbooks") {
    //Cookbooks
  }
}

もう一つの問題点(if文の中でプロパティ名を過って記述した場合Typescriptはエラーを出してくれません。)も判別可能なUnion型では改善できています。

interface Person {
  type: "person";
  name: string;
  age: number;
}
interface Book {
  type: "book";
  name: string;
  price: number;
}

function doStuff(arg: Person | Book) {
  if (arg.type === "persooon") {
    //'person'の間違え(エラーあり)
    console.log(arg.age);
  } else {
    console.log(arg.price);
  }
  console.log(arg.name);
}

またこの例ではわざとタイポしましたが、vscodeではif文内、シングルクォート(’)またはダブルクォート(")をタイプした時点でプロパティ名typeに対する値(personかbook)どちらかを推測して表示してくれるためタイポによるミスがなくなります。