as constで「魔法の文字列」をリテラル型にする
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 const | enum | |
|---|---|---|
| 実行時の出力 | ただの値(追加コードなし) | 専用オブジェクトが生成される |
| 値そのものの利用 | そのまま文字列として使える | メンバー経由になる |
| JSONとの相性 | 文字列なのでそのまま | 変換が要ることがある |
特別な理由がなければ、as const でリテラル型を作るほうがシンプルで、ビルド結果にも余計なものが増えません。たぶんこちらで困ることは少ないと思います。
まとめ
- 推論は既定で型を広げるため、決まった値のフィールドが
stringになりがち as constで「これ以上広げない・読み取り専用」をコンパイラに伝える- 値の配列やオブジェクトから
typeofと[number]/keyofで型を導けば、定義と型がずれない
「同じ文字列を3か所以上書いているな」と気づいたら、定数にまとめて as const を付け、そこから型を作るタイミングだと思います。const アサーションの詳細はTypeScript公式ハンドブックを参照してください。これでタイポにだいぶ強くなりました。以上です。