TypeScriptで条件によってプロパティの型を切り替えたり、プロパティを削除する

TypeScriptで、typeやinterface、classの総称型によって、プロパティの型を切り替えたり、削除したりすることができるのでメモ。

環境

TypeScript v4.7.4 で確認。

方法

Conditional Types という機能を用いる。日本語だと「条件付きタイプ」、「条件型」あたりか。

www.typescriptlang.org

記述は以下、条件演算子(三項演算子)に似ている。

SomeType extends OtherType ? TrueType : FalseType

上記の例だと、 SomeTypeOtherType として扱える場合 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 TypesDistributive Conditional Types についての記載もあり詳しい。

zenn.dev

振り返り

前回と同じく、コードレビューしてもらったときにレビュアーが知らなかったので書いてみた。

ライブラリやフレームワークのソースを見ると、よく使われている。何かのタイミングで、「なんだこれ?」と思って調べたのがこの機能を知ったきっかけ。

Template Literal TypesといいConditional Typesといい、型を柔軟に扱える機能だが、濫用すると可読性が下がるので注意が必要。

総称型を指定した状態で型をexportするなど、実装時に考慮をしておいたほうがいいだろう。