💮

【TypeScript】オブジェクト定数の書き方ベストプラクティス

2023/11/02に公開

じぶんはいつもTypeScriptで定数を書くときこうしてるよ〜っていう記事です。

定数とは

定数とはプログラマーが変更できない値のことです。

https://developer.mozilla.org/ja/docs/Glossary/Constant

定数を使うとなにがいい?

  • 変数を変更不可能にできる
  • 意図しない変更が発生しないように抑止することができる
  • システムの堅牢性が向上する
  • 同じ値が入る部分を定数で一元管理できる
  • 依存を一箇所に集約して修正コストを下げることができる
  • プリミティブな値に変数名で意味をもたせることができる
  • コードの可読性が向上する

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() と同じはたらきになります)

https://typescriptbook.jp/reference/values-types-variables/const-assertion

型チェック(satisfies T)と併用する

as const のうしろに satisfies { 型 } をつけることで、定数がその型と一致しているかチェックすることができます。

定数 API_ENDPOINT はオブジェクトのキーと値どちらも文字列しか入らないため、satisfies Record<string, string> をつけて型チェックが効くようにしてみます。

https://typescriptbook.jp/reference/type-reuse/utility-types/record

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")

https://typescriptbook.jp/reference/type-reuse/typeof-type-operator
https://typescriptbook.jp/reference/type-reuse/keyof-type-operator
https://typescriptbook.jp/reference/values-types-variables/union

定数から生成した型を使う

まず、定数から生成した型を使わない簡易的な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 { 型 } で定義しましょう!

参考

https://zenn.dev/moneyforward/articles/typescript-as-const-satisfies

Discussion