🧩
【型パズル】Utility Typesで、特定のオブジェクト型のみUnwrapしたい (null/undefined・ネスト・配列対応含む)
概要
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
を使えます。
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]
を使えば展開できます。
あとは再帰でポン
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