🧬

【TypeScript】特定の値のときだけoptionalプロパティがrequiredになる型定義

2022/08/31に公開

TypeScriptで特定のプロパティの値が選ばれたときだけオプショナルなプロパティを必須にする方法です。

現象

TypeScriptで型定義をしていると、以下のような型定義をしたくなることがあります。
genderプロパティの値がotherのときだけ、genderTextプロパティが必須で、それ以外の場合はgenderTextプロパティは不要

単純に考えると以下のような型定義になると思います。

type User = {
  name: string;
  age: number;
  gender: 'man' | 'woman' | 'other'
  genderText?: string;
}

上記型定義では、genderotherのときでもgenderTextが存在するかどうかをTypeScriptコンパイラが判断できないため、以下のように値が存在する前提でコードを書くとundefinedの可能性がありますと怒られてしまいます。

type User = {
  name: string;
  age: number;
  gender: 'man' | 'woman' | 'other'
  genderText?: string;
}

const user: User = {
  name: 'Taro',
  age: 30,
  gender: 'other',
  genderText: 'heterosexual'
}

console.log(user.genderText.toUpperCase()) // ERROR: Object is possibly 'undefined'.(2532)

解決策

結論

以下のように定義すると、genderotherのときはgenderTextが存在するとTypeScriptコンパイラが判断するため、エラーが出ません。

type BasicUser = {
  name: string;
  age: number;
}

type BasicGenderUser = BasicUser & {
  gender: 'man' | 'woman',
}

type OtherGenderUser = BasicUser & {
  gender: 'other',
  genderText: string,
}

type User = BasicGenderUser | OtherGenderUser

const user: User = {
  name: 'Taro',
  age: 30,
  gender: 'other',
  genderText: 'heterosexual'
}

console.log(user.genderText.toUpperCase()) // エラーが出ない

解説

まずはgenderの値に限らず共通なプロパティをBasicUser型として定義します。

type BasicUser = {
  name: string;
  age: number;
}

次に、条件分岐させたい型をそれぞれ定義します。
genderの値に応じてgendetTextの有無が確定するので、それぞれgenderTextが存在するときと存在しないときで型定義します。
ここで、BasicUserを継承することで最終的に作りたいUser型に必要なプロパティをもたせます。

type BasicUser = {
  name: string;
  age: number;
}

+ type BasicGenderUser = BasicUser & {
+   gender: 'man' | 'woman',
+ }

+ type OtherGenderUser = BasicUser & {
+   gender: 'other',
+   genderText: string,
+ }

最後に、それぞれのユニオン型としてUser型を定義することで、genderotherのときはgenderTextを持っている(OtherGenderUser)型として認識され、gendermanもしくはwomanのときはgenderTextを持っていない(BasicGenderUser)型として認識されるようになります。

type BasicUser = {
  name: string;
  age: number;
}

type BasicGenderUser = BasicUser & {
  gender: 'man' | 'woman',
}

type OtherGenderUser = BasicUser & {
  gender: 'other',
  genderText: string,
}

+ type User = BasicGenderUser | OtherGenderUser

おまけ

genderプロパティとして取り得る値(man | woman | other)を定義したい場合は、以下のようにすることできれいに書けます。

type BasicUser = {
  name: string;
  age: number;
}

type BasicGenderUser = BasicUser & {
  gender: 'man' | 'woman',
}

type OtherGenderUser = BasicUser & {
  gender: 'other',
  genderText: string,
}

type User = BasicGenderUser | OtherGenderUser
+ type Gender = BasicGenderUser['gender'] | OtherGenderUser['gender'] // type Gender = "man" | "woman" | "other"

Discussion