[TypeScript]具体例で理解する、配列から"その配列型"と"その要素のUnion型"を定義する方法とその使い道


この記事でできるようになること

  • 配列を1つ定義するだけで、それを元にその配列型とその要素のUnion型を定義することができる。

メリット

  • 配列の要素を増やすと、TypeScriptがUnionタイプの要素も自動で追加して解釈してくれる。
  • 上記により同じ情報を複数箇所で定義・管理する必要がなくなる

方法

const STATUSES = ['A','B','C'] as const
type Statuses = typeof STATUSES;
// readonly ["A", "B", "C"]
type Status = Statuses[number]
// "A" | "B" | "C"

忘れてはいけないのはas constをつけて配列を定義することである。
as constがない場合、TypeScriptは['A','B','C']A,B,Cだけからなる配列としてではなくこれを抽象的に解釈した文字列の配列(string[])として解釈してしまうからである。

Statuses[number]で配列の要素をメンバーとするUnion型を生成できる理由はsuinさんのこの記事が大変参考になった。

具体的な使い道

前提

例えば、Trelloのようなタスク管理アプリケーションを作っていたとき、
アプリ内のカードはNotStarted,In Progress,Completedという3つのステータスを持つとする。
それぞれのカードはこの状態のうちどれか1つを保持していて、ユーザーの選択によって別の2つの状態に変更できるとする。

このとき、各カードが持つステータスは次のようなUnion型で定義できる

  • カードの持つStatus型

type STATUS = 'NotStarted' | 'In Progress' | 'Completed';

次にカードの状態を選択するセレクターの描画部分は次のようにかける

const STATUSES = ['NotStarted','In Progress','Completed'];
<select
  value={task.status}
  onChange={onStatusChange}
>
  {STATUSES.map(status=>(
    <option key={status} value={status}>{status}</option>)
   )}
</select>

セレクターのイメージ

何が問題になるか

これによって、確かに目的は達成されるが、仮にステータスのオプションの種類が1つ増え以下のようになったとする。


const STATUSES = ['NotStarted','In Progress','Completed','Archive'];

このとき、STATUS型を定義している部分でも新たに'Archive'を許容するようになって欲しいが実際は以下のようにエラーが出る。これは当然で、配列STATUSESとStatus型になんの紐付けも存在しないからである。

解決方法

ここで今回の記事で紹介した方法で定義し直すと以下のようになる。

const STATUSES = ['NotStarted','Doing','Completed','Archive'] as const;
type StatusesType = typeof STATUSES;
type Status = StatusesType[number]

今回はエラーも起きず、Status型に'Archive'が動的に追加されていることが確認できる。

おわりに

今回のポイントは以下2つ。
- 配列の定義にas constを使う
- 配列型に[number]でアクセスすることでその要素のUnion型を作ることができる

後者に関しては、記事内でも紹介したようにsuinさんのこの記事が参考になった。
前者に関しては、TypeScriptのType Wideningという挙動を知るとさらに理解が深まるはずである。Type Wideningに関しては自分の場合はEffective TypeScriptが参考になった。

参考