🎳

TypeScript: 複数のオブジェクト型から、名前も型も共通するプロパティを抜き出す型演算

2022/08/18に公開

問題

typescriptのオブジェクト型が複数あるとして、名前も型も同じプロパティをどう抜き出せばいいか?
まずは2つの場合から考えてみましょう。具体的には下記のDogCat型をうまく型演算で表現したいということです。

type Dog = {
    name: string
    height: number
    weight: number
    kind: string // kindがstring型
}

type Cat = {
    name: string
    height: number
    kind: number // kindがnumber型
}

type DogCat = ?????
// DogとCatとで名前も型も同じプロパティだけ抜き出したい
// type DogCat = {
//     name: string
//     height: number
// }

答え

2通りありえますが、1つ目の方がエレガントですね👶

// 1 (mapped typeを使う)
// Pickの第2項では、名前も型も同じプロパティが自身のプロパティ名を型として、
// mapped typeを構成して(このとき、名前だけ同じで型が違うプロパティはnever型)、
// そこから名前が共通するプロパティ群をunion型で与えてプロパティを抜き出している
// (第2項のmapped typeは
//   { name: "name", height: "height", kind: never}[ "name" | "height" | "kind"]
// となり、結果的に "name" | "height" となる)
type PickSameProperties<T1, T2> = Pick<T1, {  
  [K in keyof T1 & keyof T2]: 
  	T1[K] extends T2[K] ?
  		T1[K] extends T2[K] ? K : never
  	: never;
}[keyof T1 & keyof T2]>;
type DogCat = PickSameProperties<Dog, Cat>

/////////////////////////

// 2 (union distributionを使う)
// SamePropertiesではunion distributionを使って、
// 名前も型も同じプロパティをunion型として抜き出しており、
// PickSamePropertiesでは2つの型を与えると、
// SamePropertiesで作ったunion型で一方の型からプロパティをPickする
type SameProperties<T1, T2, K extends keyof T1 & keyof T2> = NonNullable<
    K extends 
        (T1[K] extends T2[K]
            ? T2[K] extends T1[K]
                ? keyof T1
                : never
            : never)
    ? K
    : null>
type PickSameProperties<T1, T2> = Pick<T1, SameProperties<T1, T2, keyof T1 & keyof T2>>
type DogCat = PickSameProperties<Dog, Cat>

2つと言わずに複数の型に一気に適用したい場合は、下記の記事中にある型演算を組み合わせれば良さそうです👶
https://qiita.com/recordare/items/c590a0c8fb5fdf31eaf9

なお、1つ目の方法は下記で紹介されています👀
https://stackoverflow.com/questions/47375916/typescript-how-to-create-type-with-common-properties-of-two-types

使いみちの例(めちゃ個別具体的)

tiptapというwysiwygライブラリを使っているとき、ドキュメント内の要素を読み込みモードではNode型として、編集モードではMark型として扱わなければならないことがありました。
ライブラリの初期化段階ではユーザーがどちらのモードで使うか確定しないため、要素についてNode型にもMark型にも共通するプロパティだけを先に処理する必要があり、その際に今回の型演算を使いました。
(めちゃくちゃ具体的すぎてやばいですね)

GitHubで編集を提案

Discussion