👻

constants(固定値)から型を作ったり使ったりするあれこれ

2022/03/16に公開

アプリケーション内で固有の文字列、配列、オブジェクト等がある場合はそれを型にして堅牢性を高めたい。

文字列

文字列の場合は単にどこかのファイルにexport constで宣言してそれを使いたいところでimportすればいい。
型もstringなので特に考えることはないはず。

配列

配列の場合、配列に含まれる要素のリテラル型とその配列に含まれるかという操作方法が必要になると思う。

型の作成

宣言した配列をas constしてその中身の型を作る。

const authors = ['ken', 'bob', 'alice'] as const
type AuthorsType = typeof authors[number] // "ken" | "bob" | "alice"

ちなみにas constしない場合はAuthorsTypestringとみなされる。
as constしない場合、配列の中身は書き換え可能なためリテラルとみなされない。(constのためauthors自体への直接代入はできないが配列の操作自体は可能)
ただし宣言時点で配列の中身はstringしかないためstring[]とみなされる。そのためtypeofするとstringになる。

const authors = ['ken', 'bob', 'alice']
type AuthorsType = typeof authors[number] // string

更についでに書くとas constした時点でauthorsに対してpushpopのような配列を変更する操作できなくなる(ビルドエラー)

固定値に含まれるかどうかのチェック

ユーザーの入力やURLなどからのパラメータが固定値のものと合致しているか調べるときのやり方。
単純に考えてincludesすればいいだけと思うけどそうはいかない。

const authors = ['ken', 'bob', 'alice'] as const
const includeAuthors = (value: string):boolean => authors.includes(value) // Error

includesの宣言を見てみると下記のようになっている。

interface ReadonlyArray<T> {
    includes(searchElement: T, fromIndex?: number): boolean;
}

as constしたauthorsReadonlyArray<"ken" | "bob" | "alice">となるので当然第1引数もken" | "bob" | "alice"なのでその固定値以外受け取れない。

もちろんas constしていなければ普通のstring[]なのでstringを引数にとってincludesで判定できる。
なので判定にだけ使いたいとかならas constなしで宣言すればいい。

const authors = ['ken', 'bob', 'alice']
const includeAuthors = (value: string):boolean => authors.includes(value)

型も作って判定にも使いたいような場合、as constした配列をstring[]とみなしてincludesすればいい。
※もっといい感じのやり方があれば知りたい(asを使うのでスマートじゃない気がする)

const authors = ['ken', 'bob', 'alice'] as const
type AuthorsType = typeof authors[number] // "ken" | "bob" | "alice"

const func1 = (value: string):boolean => 
                    (authors as unknown as string[]).includes(value)
const func2 = (value: string):boolean => 
                    authors.map(v => v as string).includes(value)

オブジェクトの配列から型を作る場合

オブジェクトの各フィールドごとの型を作ったりする方法。

const books = [
  { id: 1, name: 'タイトル1', author: '作者1', code: 1 },
  { id: 2, name: 'タイトル2', author: '作者2', code: 2 },
  // ...
] as const

type AuthorType = typeof books[number]['author'] // "作者1" | "作者2"

配列内のオブジェクトがすべてauthorフィールドを持って定義されていればエディタでauthorは補完されるしなければauthorを書いてもエラーになる。

検索などで必要なら先にmapauthorだけの配列を作って使うこともできる。
(もともとの配列がas constになっていなければAuthorTypestringになる)

const books = [
  { id: 1, name: 'タイトル1', author: '作者1', code: 1 },
  { id: 2, name: 'タイトル2', author: '作者2', code: 2 },
  // ...
] as const
const authors = books.map(v => v.author)
type AuthorType = typeof authors[number] // "作者1" | "作者2"

オブジェクトから型を作る

const authors = {
    KEN: 'ken',
    ALICE: 'alice',
    BOB: 'bob'
} as const

type AuthorKeyType = keyof typeof authors // "KEN" | "ALICE" | "BOB"
type AuthorValType = typeof authors[AuthorKeyType] // "ken" | "alice" | "bob"
// type AuthorValType = typeof authors[keyof typeof authors] // "ken" | "alice" | "bob"

オブジェクトのvaluesを肩にする

当たり前だけど型による制御と実際の値によるチェックは全然違うものなので、大元の固定値からそれぞれの機能を実現できるように作っていく、という考え方で良さそう。

Discussion