TypeScript

as constで「魔法の文字列」をリテラル型にする

読了 6分
TypeScript

as constで「魔法の文字列」をリテラル型にする

status === 'actv' のように文字列を直接書き回すコードで、タイポに気づかずしばらくバグらせていたことがあったので、そのときのメモです。'active''actv' と打ち間違えても、型は string のままなのでコンパイルは通ってしまいます。

結論から書くと、as const とリテラル型を使うと、こうした「魔法の文字列」を型で縛れます。

型推論は既定で「広げる」

TypeScriptは値から型を推論するとき、既定では型を 広げます(widening)let で文字列を入れると、その型は string になります。

let color = 'red'
// 型は string('red' ではない)

const なら再代入されないので 'red' というリテラル型に推論されますが、オブジェクトのプロパティは中身が書き換え可能なため、やはり広げられます。

const config = { mode: 'dark' }
// config.mode の型は string

この「広げ」のせいで、決まった値しか取らないはずのフィールドが string になって、タイポを検出できなくなります。自分がハマっていたのも、たぶんこれが原因でした。

as constで「これ以上広げない」と伝える

as const を付けると、その値を 可能な限り狭く・読み取り専用 として扱うようにコンパイラへ指示できます。

const config = { mode: 'dark' } as const
// config.mode の型は 'dark'(リテラル型)
// すべてのプロパティが readonly になる

配列に付けると、要素がリテラルの タプル になって、readonly になります。

const sizes = ['sm', 'md', 'lg'] as const
// 型は readonly ['sm', 'md', 'lg']
// sizes[0] の型は 'sm'

値の集合から型を導く

as const の便利なところは、値の定義から型を自動生成できる ことです。許可する値を一か所の配列で定義して、そこから型を導けば、定義と型がずれなくなります。

const STATUSES = ['idle', 'loading', 'success', 'error'] as const

type Status = (typeof STATUSES)[number]
// => 'idle' | 'loading' | 'success' | 'error'

typeof STATUSES で値の型を取り、[number] でその要素の型を取り出しています。これで Status は4つのリテラルのユニオンになり、配列に値を足せば型も自動で追従します。これは便利です。

function setStatus(s: Status) {
  /* ... */
}

setStatus('loading') // OK、補完も効く
setStatus('lodaing') // コンパイルエラー:タイポを検出

文字列を直接書いていたときは素通りしていたタイポが、ここで止まります。さらにエディタの補完に候補が並ぶので、そもそも打ち間違えにくくなりました。

オブジェクトの定数からも作れる

設定オブジェクトを単一の真実の源にして、キーや値の型を導くこともできます。

const ROUTES = {
  home: '/',
  about: '/about',
  contact: '/contact',
} as const

type RouteName = keyof typeof ROUTES // 'home' | 'about' | 'contact'
type RoutePath = (typeof ROUTES)[RouteName] // '/' | '/about' | '/contact'

ルート定義を1か所にまとめつつ、その名前とパスを型として使い回せます。定義を増やせば型も増える、という関係が保たれるのが気持ちいいです。

enumとの比較

似た用途にTypeScriptの enum がありますが、as const 配列・オブジェクトのほうが扱いやすい場面が多いみたいです。

as constenum
実行時の出力ただの値(追加コードなし)専用オブジェクトが生成される
値そのものの利用そのまま文字列として使えるメンバー経由になる
JSONとの相性文字列なのでそのまま変換が要ることがある

特別な理由がなければ、as const でリテラル型を作るほうがシンプルで、ビルド結果にも余計なものが増えません。たぶんこちらで困ることは少ないと思います。

まとめ

  • 推論は既定で型を広げるため、決まった値のフィールドが string になりがち
  • as const で「これ以上広げない・読み取り専用」をコンパイラに伝える
  • 値の配列やオブジェクトから typeof[number] / keyof で型を導けば、定義と型がずれない

「同じ文字列を3か所以上書いているな」と気づいたら、定数にまとめて as const を付け、そこから型を作るタイミングだと思います。const アサーションの詳細はTypeScript公式ハンドブックを参照してください。これでタイポにだいぶ強くなりました。以上です。