TypeScriptで、typeやinterface、classの総称型によって、プロパティの型を切り替えたり、削除したりすることができるのでメモ。
環境
TypeScript v4.7.4 で確認。
方法
Conditional Types
という機能を用いる。日本語だと「条件付きタイプ」、「条件型」あたりか。
SomeType extends OtherType ? TrueType : FalseType
上記の例だと、 SomeType
が OtherType
として扱える場合 TrueType
、扱えない場合 FalseType
となる。
実装例
こんなUnionを用意。
type ExampleUnion = 'one' | 'two' | 'three'
プロパティの型の切り替え
type
等で総称型を指定できるようにして、型を切り替えられる。
type ExampleType<T extends ExampleUnion> = { value: T extends 'one' ? string : number } // Recordの第2引数など、型を記述できるところであればどこでも使える & Record<"records", Array<T extends 'two' ? number : string>> // ここは丸かっこで囲わないと、最初の設定が消える & (T extends 'three' ? { three: string } : { other: number }) const one: ExampleType<'one'> = { value: 'one', records: ['one'], other: 1, } const two: ExampleType<'two'> = { value: 2, records: [2], other: 2, } const three: ExampleType<'three'> = { value: 3, records: ['three'], three: 'three', }
また、Mapped Typesを宣言するときも、 extends
で一部のプロパティの型を変更できる。
type ExampleType = { [P in ExampleUnion]: P extends 'one' ? string : number } const example: ExampleType = { one: 'one', two: 2, three: 3, }
プロパティの削除
Omit
の第2引数で判定し、対象となるプロパティを指定する。
また、型の切り替えの例のように、交差型を使ってもいい。
type ExampleType<T extends ExampleUnion> = Omit< { value: string values: string[] }, T extends 'one' ? 'values' : T extends 'two' ? 'value' : never > const one: ExampleType<'one'> = { value: 'one', } const two: ExampleType<'two'> = { values: ['two'], } const three: ExampleType<'three'> = { value: 'three', values: ['three'], }
参考
以下のページが、 Inferring Within Conditional Types
や Distributive Conditional Types
についての記載もあり詳しい。
振り返り
前回と同じく、コードレビューしてもらったときにレビュアーが知らなかったので書いてみた。
ライブラリやフレームワークのソースを見ると、よく使われている。何かのタイミングで、「なんだこれ?」と思って調べたのがこの機能を知ったきっかけ。
Template Literal TypesといいConditional Typesといい、型を柔軟に扱える機能だが、濫用すると可読性が下がるので注意が必要。
総称型を指定した状態で型をexportするなど、実装時に考慮をしておいたほうがいいだろう。