🙆

[TypeScript] 「このプロパティを渡すならこのプロパティも必須だよ」を実現する Generics

2024/06/27に公開

コードから

type MinimalProps = {
  prop1: string;
  prop2: string;
  prop3?: string; 
};
type WithMaskProps = MinimalProps & {
  maskProp1: string;
  maskProp2: string;
  maskProp3?: string;
}
type ExcludeUnknownProps<T, U> = {
  [K in keyof U as K extends keyof T ? K : never]: K extends keyof T
    ? T[K]
    : never;
} & Partial<{
  [K in keyof U as K extends keyof T ? never : K]: never;
}>;
type Props =
  | ExcludeUnknownProps<MinimalProps, WithMaskProps>
  | WithMaskProps;

↓↓↓

すると、これは OK

const validProps1: Props = {
  prop1: 'aaa',
  prop2: 'iii',
  prop3: 'uuu'
};
const validProps2: Props = {
  prop1: 'aaa',
  prop2: 'iii',
  prop3: 'uuu',
  maskProp1: 'eee',
  maskProp2: 'ooo'
};

これは Error

const invalidProps: Props = {
  prop1: 'aaa',
  prop2: 'iii',
  prop3: 'uuu',
  maskProp1: 'eee'
  // maskProp2 がないよ!!
};

イイネ!!

なぜ ExcludeUnknownProps が必要なの?

ExcludeUnknownProps を使わない場合、上の invalidProps はエラーになりません。

type UnexpectedProps = MinimalProps | WithMaskProps;
const invalidProps: UnexpectedProps = {
  prop1: 'aaa',
  prop2: 'iii',
  prop3: 'uuu',
  maskProp1: 'eee'
  // maskProp2 がないのにエラーにならないよ!!
};

MinimalProps が暗黙的にインデックスシグネチャを持っているので知らないプロパティを全て受け付けてしまうためです。(ちょっとこの説明自信ない)

ExcludeUnknownProps は何してるの?

ExcludeUnknownProps は2つの型を受け取ります。
2つ目の型のプロパティの中で、1つ目の型に存在しないプロパティは never を設定します。
ExcludeUnknownProps<MinimalProps, WithMaskProps> の場合、 MinimalProps に存在しないプロパティは渡しちゃダメ、ということになります。
これと組み合わせて ExcludeUnknownProps<MinimalProps, WithMaskProps> | WithMaskProps とすることで、 MinimalProps だけの props or WithMaskProps を全部持つ props のどちらかを受け付けるようになります。

Partial<{
  [K in keyof U as K extends keyof T ? never : K]: never;
}>

ここの Partial ですが、 optional にしないと never 型を渡しなさい、といわれてしまうので Partial でラップしています。

おわりに

こんなコード自分では書けません。
今日もありがとう、ChatGPT。

Discussion