🧩

【型パズル】Utility Typesで、特定のオブジェクト型のみUnwrapしたい (null/undefined・ネスト・配列対応含む)

2023/06/05に公開

概要

type DataType = { data: string } | null | undefined

を、

type DataInputType = string | null | undefined

に変換できる

type DataInputType = Hoge<DataType>

みたいな型Hoge<T>が欲しい

経緯

こういう感じでデータを持つ場合を考えた時、

const book: Book = {
  type: "Book",
  data: {
    id: {
      type: "ID",
      data: "book-1",
    },
    name: {
      type: "String",
      data: "Harry Potter",
    }
  },
}

それをcreateする関数を作るとして、

const createBook = (input: CreateBookInput): Book {
  return ....
}

inputの型はこうなっていて欲しい。

type CreateBookInput = {
  id: string;
  name: string;
};

が、Book型をそのまま使うとこうなってしまう

type CreateBookInput = {
  type: string;
  data: {
    id: {
      type: string;
      data: string;
    };
    name: {
      type: string;
      data: string;
    };
  };
};

ので、うまいこと自動生成したい。

NonNullableな場合

Conditional Typesで、 infer を使えます。
https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types

type Handle<T> = T extends { data: infer K } ? K : T;
type A1 = Handle<{ data: string }>; // string
type A2 = Handle<number>; // number

ネスト

Objectは [K in keyof T] を使えば展開できます。
https://www.typescriptlang.org/docs/handbook/2/mapped-types.html

あとは再帰でポン

type Handle<T> = T extends { data: infer K } ? Spread<K> : T;
type Spread<T> = {
  [K in keyof T]: Handle<T[K]>;
};
type A3 = Handle<{ data: { name: { data: string } } }>; // { name: string }

配列

Array でハンドリングします。

type Handle<T> = T extends { data: infer K }
  ? Spread<K>
  : T extends Array<infer M>
  ? Array<Handle<M>>
  : T;
type Spread<T> = {
  [K in keyof T]: Handle<T[K]>;
};
type A4 = Handle<{ data: string }[]>; // string[]

Nullableな場合

null/undefinedとUnion

こちらもConditional Typesでがんばります。

type HandleNullable<T> = T extends null
  ? 'null'
  : T extends undefined
  ? 'undefined'
  : 'string';

こんなふうに書けば、Union型も勝手にいい感じになります。

type B1 = HandleNullable<string>; // 'string'
type B2 = HandleNullable<string | null>; // 'string' | 'null'
type B3 = HandleNullable<string | undefined>; // 'string' | 'undefined'
type B4 = HandleNullable<string | null | undefined>; // 'string' | 'null' | 'undefined'

まとめ

これとさっきの諸々をまとめるとこうなります。

type HandleNullish<T> = T extends null
  ? null
  : T extends undefined
  ? undefined
  : T extends { data: infer K }
  ? Spread<K>
  : T extends Array<infer M>
  ? Array<HandleNullish<M>>
  : T;

type Spread<T> = {
  [K in keyof T]: HandleNullish<T[K]>;
};

以下動作例です。

// { data: string } は stringに展開される
type T1 = HandleNullish<{ data: string }>; // string
type T2 = HandleNullish<{ data: string } | null>; // string | null
type T3 = HandleNullish<{ data: string } | undefined>; // string | undefined
type T4 = HandleNullish<{ data: string } | null | undefined>; // string | null | undefined

// { data: string } ではない型はそのまま
type T5 = HandleNullish<number>; // number
type T6 = HandleNullish<number | null>; // number | null
type T7 = HandleNullish<number | undefined>; // number | undefined
type T8 = HandleNullish<number | null | undefined>; // number | null | undefined

// 配列は展開
type T9 = HandleNullish<{ data: string }[]>; // string[]
type T10 = HandleNullish<({ data: string } | null)[]>; // (string | null)[]
type T11 = HandleNullish<({ data: string } | undefined)[]>; // (string | undefined)[]
type T12 = HandleNullish<({ data: string } | null | undefined)[]>; // (string | null | undefined)[]

// これも当然大丈夫
type T13 = HandleNullish<{ data: string }[] | null>; // string[] | null
type T14 = HandleNullish<{ data: string }[] | undefined>; // string[] | undefined
type T15 = HandleNullish<{ data: string }[] | null | undefined>; // string[] | null | undefined

// これも大丈夫
type T16 = HandleNullish<({ data: string } | number)[]>; // (string | number)[]

// ネストも展開
type T17 = HandleNullish<{ data: { name: { data: string } } }>; // { name: string }
type T18 = HandleNullish<{ data: { name: { data: string | null } } } | undefined>; // { name: string | null } | undefined

まとめ

  • 3日溶かした

Discussion