【TypeScript】オブジェクト定数の書き方ベストプラクティス
じぶんはいつもTypeScriptで定数を書くときこうしてるよ〜っていう記事です。
定数とは
定数とはプログラマーが変更できない値のことです。
定数を使うとなにがいい?
- 変数を変更不可能にできる
- 意図しない変更が発生しないように抑止することができる
- システムの堅牢性が向上する
- 同じ値が入る部分を定数で一元管理できる
- 依存を一箇所に集約して修正コストを下げることができる
- プリミティブな値に変数名で意味をもたせることができる
- コードの可読性が向上する
TypeScriptで定数を定義してみる
使用する開発環境やフレームワークにもよりますが、基本的に定数を定義するファイルは costants
ディレクトリにまとめて格納しましょう。
# 例
src/constants/{ 定数の分類名 }.ts
# APIに関する定数の場合
src/constants/api.ts
APIエンドポイントのパス文字列をオブジェクト定数として定義した例を見てみましょう。
export const API_ENDPOINT = {
AUTH: '/auth',
GET_MEMBER: '/get-member',
} as const
// ↓↓↓ 生成される型
// {
// readonly AUTH: "/auth";
// readonly GET_MEMBER: "/get-member";
// }
変数名は大文字スネークケースとし、オブジェクト定義の末尾に as const
をつけます。
as const
をつけることですべてのプロパティを再帰的に読み取り専用( readonly
)にし、数値や文字列はリテラル型で固定されます。(JSにおける deepFreeze()
と同じはたらきになります)
satisfies T
)と併用する
型チェック(as const
のうしろに satisfies { 型 }
をつけることで、定数がその型と一致しているかチェックすることができます。
定数 API_ENDPOINT
はオブジェクトのキーと値どちらも文字列しか入らないため、satisfies Record<string, string>
をつけて型チェックが効くようにしてみます。
export const API_ENDPOINT = {
AUTH: '/auth',
GET_MEMBER: '/get-member',
} as const satisfies Record<string, string>
試しに Record<string, string>
と一致しない number
型の値を追加してみると、
export const API_ENDPOINT = {
AUTH: '/auth',
GET_MEMBER: '/get-member',
HOGE: 123,// -> Type 'number' is not assignable to type 'string'.
} as const satisfies Record<string, string>
ちゃんと値が string
かどうかチェックされていることがわかります。
Type 'number' is not assignable to type 'string'.
定数からその型を定義する
typeof { 定数の変数名 }
とすることで定数そのものの型を、keyof typeof { オブジェクト定数の変数名 }
とすることでオブジェクト定数のキーのユニオン型を生成することができます。
これにより、オブジェクト定数のキーと値の型を変数から生成することができるため、定数側を変更しただけで自動的に型定義も反映されるようになります。
import { API_ENDPOINT } from '~/constants/api.ts'
export type ApiEndPointKey = keyof typeof API_ENDPOINT
// -> ("AUTH" | "GET_MEMBER")
export type ApiEndPoint = typeof API_ENDPOINT[ApiEndPointKey]
// -> ("/auth" | "/get-member")
定数から生成した型を使う
まず、定数から生成した型を使わない簡易的なAPI呼び出し関数を例に見てみましょう。
安直に考えると fetchApi
関数の引数 endpoint
には文字列が入るので string
で定義してしまいがちですが、この場合引数には文字列であれば何でも入る状態になってしまいます。
(実際にはベタ書きせず定数を直接入れると思いますが、例としてあえてこのようにしています)
const fetchApi = async (endpoint: string) => {
const response = await fetch(`/api${ endpoint }`)
const data = await response.json()
return data
}
fetchApi('/auty')// -> タイプミスしても怒られない
fetchApi('/hoge')// -> 存在しないエンドポイントを渡しても怒られない
fetchApi(API_ENDPOINT.AUTH)// OK!(だけど型チェックされているわけではない)
ここで、さきほど定数から生成した型を endpoint
に指定してみましょう。
const fetchApi = async (endpoint: ApiEndPoint) => {
const response = await fetch(`/api${ endpoint }`)
const data = await response.json()
}
fetchApi('/auty')// -> Argument of type '"/auty"' is not assignable to parameter of type 'ApiEndPoint'
fetchApi('/hoge')// -> Argument of type '"/hoge"' is not assignable to parameter of type 'ApiEndPoint'
fetchApi(API_ENDPOINT.AUTH)// OK!
エンドポイント文字列に型チェックが効いて、定数で定義されている値以外を入れた場合に怒ってくれるようになりました!
まとめ
- 定数を使って一元管理しましょう!
- オブジェクト定数は
as const satisfies { 型 }
で定義しましょう!
参考
Discussion