TypeScript は便利だ。型検査で値が保証されるのはとても頼もしい。
とはいえ場合によっては型検査を通すために不必要にタイプ量が増えてしまうことがある。
例えば下記のような型が 外部ライブラリによって生成される としよう。
type User = { __typename: 'user'; id: string; name: string; gender: 'male' | 'female'; department: { __typename: 'department'; id: string; name: string; }; };
いま User 型の値を生成して使いたい。ただし、とても重要な前提条件として キー __typename には関心がない とする。
const user: User = { __typename: 'user', // このキーは使うつもりがないし関心もない id: 'u001', name: 'SUZUKI Ichiro', gender: 'male', department: { __typename: 'department', // このキーは使うつもりがないし関心もない id: 'd001', name: 'sales', } };
このような値を定義すれば型検査をパスする。
が、使うつもりのない __typename をわざわざ書くのは面倒だと思った、そう仮定しよう。
特定のキーを省略可能にしたい
type User においてキー __typename は必須だった。もしも何らかの方法で以下のような型 BetterUser を生成できたら。
type BetterUser = { __typename?: 'user'; id: string; name: string; gender: 'male' | 'female'; department: { __typename?: 'department'; id: string; name: string; }; };
__typename?: となっているのに注目してもらいたい。キー __typename は省略可能なので以下のような値でも受け入れられる。
const user: BetterUser = { // __typename を書かなくて済む id: 'u001', name: 'SUZUKI Ichiro', gender: 'male', department: { // __typename を書かなくて済む id: 'd001', name: 'sales', } };
いかにも良さそうだ。__typename を省略可能にするにはどうすればいいだろうか。
ユーティリティタイプでどうにかする
既存の型をもとに別の型を生成する 型レベル関数 のようなものを TypeScript においては「ユーティリティタイプ」と呼んでいる。1
例えば、全てのキーを省略可能するユーティリティタイプ Partial<T> は次のように書ける。
type Partial<T> = { [K in keyof T]?: T[K] };
動作イメージはこうだ、
type User = { __typename: 'user'; id: string; name: string; gender: 'male' | 'female'; }; type PartialUser = Partial<User>; // => type PartialUser = { // __typename?: 'user' | undefined; // id?: string | undefined; // name?: string | undefined; // gender?: 'male' | 'female' | undefined; // };
実はこのようなユーティリティタイプ Partial<T> は TypeScript に最初から用意されている。そのためわざわざ定義しなくても使える。とても便利だ。
だが今回は 指定したキーだけ 省略可能にしたいのだった。全てのキーを省略可能する Partial<T> は少しやりすぎだ。
Partial<T> をそのまま使うことは出来なそうだが「指定したキーだけ省略可能にするユーティリティタイプを定義して使う」という方針は決まった。
先に名前を決めてしまおう SelectivePartial というユーティリティタイプが欲しい。
type SelectivePartial<T, K> = {}; // TODO: 実装する type BetterUser = SelectivePartial<User, '__typename'>;
このように書けると良さそうだ。
Partial を応用した単純な例
指定したキーだけ省略可能にするのはわりと簡単で、ChatGPT に頼むとサラッと書いてくれる。
type SelectivePartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
このように。
Omit<T, K> も Pick<T, K> も TypeScript に標準のユーティリティタイプだ。Omit<T, K> は K で指定したキーを T から除外してくれる。Pick<T, K> はその逆で、K で指定したキーのみ T から残してくれる。
type User = { __typename: 'user'; id: string; name: string; gender: 'male' | 'female'; }; type Omitted = Omit<User, '__typename'>; // User から __typename を除外 // => type Omitted = { // id: string; // name: string; // gender: 'male' | 'female'; // }; type Picked = Pick<User, '__typename'>; // User から __typename だけを残す // => type Picked = { // __typename: 'user'; // };
これと先ほどの Partial<T> を組み合わせて、さらに & でつないで交差型 (Intersection Types) を作ると「指定したキーだけ省略可能にする」が実現できる。
type Parted = Partial<Pick<User, 'name'>>; // name だけを残し、省略可能にする // => type Parted = { // __typename?: 'user' | undefined; // }; type BetterUser = Omitted & Parted; // => type BetterUser = { // __typename?: 'user' | undefined; // id: string; // name: string; // gender: 'male' | 'female'; // };
一連の処理をまとめて型変数を使って書くと最初に示した SelectivePartial<T, K> の定義になる。
type SelectivePartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; type BetterUser = SelectivePartial<User, '__typename'>; // => type BetterUser = { // __typename?: 'user' | undefined; // id: string; // name: string; // gender: 'male' | 'female'; // };
最低限の要件を満たしてはいるが不満が残る。この方法ではネストしたオブジェクト型に再帰的に作用させることはできない。
SelectivePartial を再帰的に作用させたい
最初の例をもう一度見てみよう。最初に示した User 型はこんな感じだった。
type User = { __typename: 'user'; id: string; name: string; gender: 'male' | 'female'; department: { __typename: 'department'; id: string; name: string; }; };
SelectivePartial<T, K> を User に作用させると次のようになる。
type SelectivePartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; type BetterUser = SelectivePartial<User, '__typename'>; // => type BetterUser = { // __typename?: 'user' | undefined; // id: string; // name: string; // gender: 'male' | 'female'; // department: { // __typename: 'department'; // id: string; // name: string; // }; // };
department の中の __typename はあいかわらず省略できない。
ユーティリティタイプを再起的に作用させるには、関数の再帰呼び出しのアイデアを借用すればいい。と、その前に Omit<T, K> と Partial<Pick<T, K>> を自前の実装で書き直す必要がある。
Omit<T, K> と Pick<T, K> とても似たコードで実装できる。
type Omit<T, K> = { [L in keyof T as L extends K ? never : L]: T[L]; }; type Pick<T, K> = { [L in keyof T as L extends K ? L : never]: T[L]; };
どちらの実装も L in keyof T の部分で T の各キーに対して作用を起こしている。
Omit の方では L extends K ? never : L で「キー L が K に含まれていればキー L を消し去る」という作用を表現している。Pick はその逆で、L extends K ? L : never で「キー L が K に含まれていなければキー L を消し去る」という作用を表現している。
Pick<T, K> の自前実装をもとに Partial<Pick<T, K>> も自前で書き下したい。じつはこれはとても簡単で、キーの後ろに ? を打って省略可能であることを表明するだけだ。
type PartialPick<T, K> = { [L in keyof T as L extends K ? L : never]?: T[L]; };
ここまでのコードをまとめて前の節で書いた再帰的でない SelectivePartial<T, K> を書き直してみよう。
// type SelectivePartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; type SelectivePartial<T, K> = { [L in keyof T as L extends K ? never : L]: T[L]; } & { [L in keyof T as L extends K ? L : never]?: T[L]; };
ここまでくればユーティリティタイプを "再帰呼び出し" するのは簡単だ。T[L] の部分をちょっといじってあげればよい。
type SelectivePartial<T, K> = { [L in keyof T as L extends K ? never : L]: SelectivePartial<T[L], K>; } & { [L in keyof T as L extends K ? L : never]?: SelectivePartial<T[L], K>; };
改良版の SelectivePartial<T, K> を使ってみよう。
type User = { __typename: 'user'; id: string; name: string; gender: 'male' | 'female'; department: { __typename: 'department'; id: string; name: string; }; }; type BetterUser = SelectivePartial<User, '__typename'>; // => type BetterUser = { // __typename?: 'user' | undefined; // id: string; // name: string; // gender: 'male' | 'female'; // department: { // __typename?: 'department' | undefined; // id: string; // name: string; // }; // };
かなり良くなった。
配列型にも適用したい
もしも最初に手元にあるのが User 型ではなく Users 型だったらどうなるだろうか。
type Users = { __typename: 'user'; id: string; name: string; gender: 'male' | 'female'; department: { __typename: 'department'; id: string; name: string; }; }[];
先ほどとよく似ているが [] がついている。「オブジェクト型の配列」型だ。先ほど定義した SelectivePartial<T, K> を使って Users から BetterUsers を作れるだろうか。
TypeScript の型についてのちょっとしたテクニックを知っていれば T[] と T を相互変換できる。
type A = T[]; type B = A[number]; // ^? T type C = T; type D = C[]; // ^? T[]
T[] 型の A に対して A[number] とすると T 型が得られる。慣れないと奇妙な感じがするが、イデオムとして覚えてしまうと便利だ。
C = T に対して C[] と書くと T[] が得られるのは、まぁ、自明だろう。
このテクニックを使うと SelectivePartial<T, K> を Users に適用できるようになる。
type Users = { __typename: 'user'; id: string; name: string; gender: 'male' | 'female'; department: { __typename: 'department'; id: string; name: string; }; }[]; type User = Users[number]; type BetterUser = SelectivePartial<User, '__typename'>; type BetterUsers = BetterUser[]; // => type BetterUsers = { // __typename?: 'user' | undefined; // id: string; // name: string; // gender: 'male' | 'female'; // department: { // __typename?: 'department' | undefined; // id: string; // name: string; // }; // }[];
もちろんひとまとめにして次のように書いても同じ意味になる。
type BetterUsers = SelectivePartial<Users[number], '__typename'>[];
まとめ
Omit<T, K> & Partial<Pick<T, K>> というテクニックは ChatGPT がすぐに教えてくれた。それを再帰的に適用するにはいくつか発想の転換が必要だった。
いまはスッキリ理解できていても TypeScript を書かずにいる期間が空くとすぐに筋力が衰えてしまう。少しでも忘却に抗うためにここに書き記す。
私からは以上です。
おまけ
今回書いたコードがここにある、
- TypeScript にはさまざまな 組み込みのユーティリティタイプ がある、知っておくといつか何かの役に立つかもしれない↩