🗯️

TypeScriptでオブジェクトにある値だけを許容する型を作成する

2022/07/31に公開

TypeScriptでオブジェクトにある値だけを許容する型を作成して活用する方法です

まず結論

以下の例のように書くと、型であるfileDataTypeはオブジェクトFILE_DATA_LISTのプロパティに設定されている値(1,2,3)しか許容しなくなります

const FILE_DATA_LIST = {
 fileDataNo1: 1,
 fileDataNo2: 2,
 fileDataNo3: 3
} as const

type fileDataType = typeof FILE_DATA_LIST[keyof typeof FILE_DATA_LIST]

下記のように、1,2,3以外の値を代入しようとすると、エラーで教えてくれます

// OK
const exampleFileNo1:fileDataType = 1 

// エラー出してくれる
// Type '4' is not assignable to type 'fileDataType'
const exampleFileNo4:fileDataType = 4

実際に試したい人はこちら(TypeScript Playground)
https://www.typescriptlang.org/ja/play?#code/MYewdgzgLgBAYgSQDIFED6ARAggFS2pBAZRxgF4YBvAKBgDMBLAGwFMMBDKdgORAEYAXDD4AaWo1YcuvAExCZY+szaceIAMxD1YgL4x2EGKEhRqUAJ4AHFksmqcVmxQvWQdeMnTY8BYjgDaANYs5m4wLixhiKiYuPiEJAC61NQA9KkwAPIA0tTG0DAsAB7sALaWrHDKvIISKlwO1uTCMCnpMIAVDICXDIA-DIBfioDqDIBmDIDyDIAxDIDRDHngBcVlFSxVrLwALAJ1UuyNTjArQA

説明

このやり方ではTypeScriptのas constkeyoftypeofを組み合わせています

1.const as constをオブジェクトに指定する

オブジェクトを代入しているのに加えて、後ろにas constをつけています

as constをつけない場合、プロパティの値はどうなるでしょうか

const FILE_DATA_LIST = {
 fileDataNo1: 1, // 'number' 型
 fileDataNo2: 2, // 'number' 型
 fileDataNo3: 3, // 'number' 型
}

プロパティの値はプリミティブ(number型)となります

これはTypeScriptがオブジェクトのプロパティの値は変わるものとして扱う為です。

(これをリテラルタイプwideningと言います)

// どうせ違う値が入るだろうからnumber型にしとくわ
// なので、number型であれば代入することもできる
FILE_DATA_LIST.fileDataNo1 = 4 // 'number' 型

これに対しas constをつけると、readonlyプロパティが付与されます

これにより、値そのものを型として扱う事ができます

const FILE_DATA_LIST = {
 fileDataNo1: 1, // 'fileDataNo1:1' 型になる
 fileDataNo2: 2, // 'fileDataNo1:2' 型になる
 fileDataNo3: 3, // 'fileDataNo1:3' 型になる
} as const

なので、代入しようとするとエラーがでるようになります

// as constで定義したものは代入することはできない
FILE_DATA_LIST.fileDataNo1 = 4
// Cannot assign to 'fileDataNo1' because it is a read-only property.(2540)
(参考)型アサーション(as)とは?

2.keyofを指定する

keyofを使うとtypeなどで型定義されたものに対して、プロパティ名のユニオン型を取得できます

type FILE_DATA_LIST = {
 fileDataNo1: number
 fileDataNo2: number
 fileDataNo3: number
}

type list = keyof FILE_DATA_LIST;

// プロパティ名のユニオン型、つまりこうなります(上と同義)
type list = "fileDataNo1" | "fileDataNo2" | "fileDataNo3";

3.typeofを指定する

最後にtypeofですが、これは与えられた値から型を取得することができるものです

let FileName = "hogehoge"
let FileNo = 20

// "hogehoge"という値から、string型と判断。これをFileNameTypeに代入する
type FileNameType = typeof FileName // string型

// 20という値から、number型と判断。これをFileNameTypeに代入する
type FileNoType = typeof FileNo // number型

以上を組み合わせると...

ここでもう一度、いちばん最初に記載したコードを見てみます

const FILE_DATA_LIST = {
 fileDataNo1: 1, // as constにより'fileDataNo1:1' 型となる
 fileDataNo2: 2, // as constにより'fileDataNo1:2' 型となる
 fileDataNo3: 3, // as constにより'fileDataNo1:3' 型となる
} as const

type fileDataType = typeof FILE_DATA_LIST[keyof typeof FILE_DATA_LIST]

どうでしょうか。as constは型が変わっている事が分かりやすいですね

問題はこの部分です

typeof FILE_DATA_LIST[keyof typeof FILE_DATA_LIST]

まず[keyof typeof FILE_DATA_LIST][]で囲まれている部分

[keyof typeof FILE_DATA_LIST]
// keyofとtypeofと組み合わせる事でユニオン型になる
// "fileDataNo1" | "fileDataNo2" | "fileDataNo3"

そして、上記のユニオン型にtypeof FILE_DATA_LISTの"添字"として上記ユニオン型を渡します

typeof FILE_DATA_LIST["fileDataNo1" | "fileDataNo2" | "fileDataNo3"]
// ユニオン型のそれぞれの型をキーとしてFILE_DATA_LISTの値を取り出す
// つまり、これになる
// 1 | 2 | 3

これで、オブジェクトにある値だけを許容する型(fileDataType)が完成しました🎊

✨何が嬉しいのか

上書きする事ができない

上記のas constと重複しますが、他の場所で変数を上書きする事ができません

const FILE_DATA_LIST = {
 fileDataNo1: 1
 fileDataNo2: 2
} as const
type fileDataType = typeof FILE_DATA_LIST[keyof typeof FILE_DATA_LIST] 

let exampleFileNo:fileDataType 
exampleFileNo = FILE_DATA_LIST.fileDataNo1  // 1 しか許容しない
exampleFileNo = 1000 // Type '1000' is not assignable to type '1'

なのでas constで定義されている値だけ見れば、変数に何の値が代入されているのを確認できますし、他の場所で書き換えられていない事が保証されるので、精神的にも非常に楽です

型定義を変更する必要がない

嬉しい所の2つ目は、値から型を作成しているところです

例えば、今までの3つのファイル定義に加えて、4つ目のファイル定義が必要になったとします

この場合、オブジェクトに4つ目のファイル定義をするだけでOKです

const FILE_DATA_LIST = {
 fileDataNo1: 1,
 fileDataNo2: 2,
 fileDataNo3: 3,
 fileDataNo4: 4 // ここに加えるだけ!!
} as const

type fileDataType = typeof FILE_DATA_LIST[keyof typeof FILE_DATA_LIST]

1 | 2 | 3 | 4だけ許容した型ができる

これを型定義から行った場合、変更がある度に型定義まで変更しないといけなくなります。

❌よくない

// 型定義を最初に行うパターン
type fileDataList = "fileDataNo1" | "fileDataNo2" | "fileDataNo3" 
type faileDatavalue = 1 | 2 | 3

const FILE_DATA_LIST: Record<fileDataList, faileDatavalue> = {
 fileDataNo1: 1,
 fileDataNo2: 2,
 fileDataNo3: 3,
 // 足そうとすると、上で定義した型まで変えないといけなくなってしまう
 fileDataNo4: 4
}

値から型を作成するパターンの方が変更も一箇所で済みますし、可読性も上がります。

他の修正箇所に気づきやすくなる

また、他の修正するべき箇所に気づきやすくなります

例えばswitchで処理を分岐するシーンが出てきたとします

❌よくない

const FILE_DATA_LIST = {
 fileDataNo1: 1,
 fileDataNo2: 2,
 fileDataNo3: 3,
 fileDataNo4: 4 // 4つ目を追加
}

// 引数にfileDataNo4(数字の4)を渡してdataUpload関数を実行
dataUpload(FILE_DATA_LIST.fileDataNo4);

switch文にfileDataNo4の場合を追記していないのでdefault節に処理が流れてしまいます...

function dataUpload(fileDataNo:number){  
  switch(fileDataNo){
   case FILE_DATA_LIST.fileDataNo1:
      // 1番目の処理
     break;
   case FILE_DATA_LIST.fileDataNo2:
      // 2番目の処理
     break;
   case FILE_DATA_LIST.fileDataNo3:
      // 3番目の処理
     break;
   // defaultの処理がされてしまうが、エラーにはならない
   default:	   
     break;
  }
}

数行なら気付けるかもしれませんが、大量のコードがある中でピンポイントで気付くのは中々大変な作業です

⭕️default節にnever型をセットする

const FILE_DATA_LIST = {
 fileDataNo1: 1,
 fileDataNo2: 2,
 fileDataNo3: 3,
 fileDataNo4: 4 // 4つ目を追加
} as const
type fileDataType = typeof FILE_DATA_LIST[keyof typeof FILE_DATA_LIST]

// 引数にfileDataNo4(数字の4)を渡してdataUpload関数を実行
dataUpload(FILE_DATA_LIST.fileDataNo4);

function dataUpload(fileDataNo:fileDataType){
  switch(fileDataNo){
   case FILE_DATA_LIST.fileDataNo1:
      // 1番目の処理
     break;
   case FILE_DATA_LIST.fileDataNo2:
      // 2番目の処理
     break;
   case FILE_DATA_LIST.fileDataNo3:
      // 3番目の処理
     break;
   default:
     const imageFileNo: never = fileDataNo;
     // Type 'number' is not assignable to type 'never'エラーになる
     throw new Error(${imageFileNo} は存在しません);
  }
}

変更点

  • dataUpload関数の引数にfileDataTypeを指定する
  • defaultの処理の中でnever型を指定する

fileDataTypeで型を定義している以上default節に処理が来ることはあり得ないので、fileDataNo4が追加されるとエラーになります

この様にneverなどの他のTypeScriptの型を組み合わせることで、値を追加・変更した場合の他の修正箇所が分かりやすくなり、開発がしやすくなります。

まとめ

オブジェクトで定義した値のみを許容する型を作りたいときは、as constkeyoftypeofを組み合わせて使おう

Discussion