TypeScriptのOmitの実装を見てみよう
TypeScriptを使い始めて2年以上が経過し、ふと普段からよく使うPick
とかPartial
などのユーティリティタイプががどうやって実装されているか知らないなぁと思い、調べて見ました。
本記事ではTypeScriptのユーティリティタイプの1つであるOmit
の実装を見ていきたいと思います。
目次
- ユーティリティタイプとは?
- Omitとは?
- Omitの実装
- Omitの問題点&改善案
- OmitStrict?
- まとめ
ユーティリティタイプとは?
ユーティリティタイプとは何なのかというのを軽く紹介したいと思います。
ドキュメントには以下のように書かれています。
TypeScript provides several utility types to facilitate common type transformations.
These utilities are available globally.
https://www.typescriptlang.org/docs/handbook/utility-types.html より引用
意訳するとこんな感じです。
TypeScriptはよくある型を作成するのに便利な型を用意してる。しかもそれはグローバルで使えるで
例えば以下のようなものがあります。
- Pick
- Partial
- Record
- Exclude
- Omit
- ReturnType
などなど。他にもたくさんあります。
今回これらユーティリティタイプの中から、Omit
の実装を見ていきたいと思います。
Omitとは?
まずOmitとは何なのかということですが、日本語では「省く」といった意味で、ある型から一部のプロパティを省いて新しい型を作成することができます。
例えば以下のような User
という型があるとします。
type User = {
id: number;
name: string;
nickname: string;
age: number;
lang: 'Ja' | 'En';
level: number;
};
このUserという型からageを除外し、UserWithoutAge
という型を作りたい場合、以下のような感じでOmit
を使うことができます。
type UserWithoutAge = Omit<User, 'age'>;
UserWithoutAgeの実態はこのようになっています。
type UserWithoutAge = {
id: number;
name: string;
nickname: string;
lang: 'Ja' | 'En';
level: number;
};
ageというプロパティがなくなっていますね。
Omitの実装
それではこのOmit
の実装を見ていきたいと思います。
実装はこのようになっています。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
別のユーティリティタイプのPick
やExclude
が使われていることからOmit
はユーティリティタイプの中では後発だということが分かりますね。
除外したいプロパティ以外のユニオンタイプをExcludeを使って作成、それからそのユニオンタイプとPickを使って、除外したいプロパティ以外のプロパティを持つ型を作成しています。
type UserWithoutAge = Omit<User, 'age'>;
先ほど紹介したUserWithoutAge
を作成する流れは以下のようになっています。
前提: T = User, K = age
1: Exclude<keyof T, K>でage以外のUserのプロパティのユニオンタイプを作成
'id' | 'name' | 'nickname' | 'lang' | 'level'
2: 1で作成したユニオンタイプとPickを使って除外したいプロパティ以外のプロパティを持つ型を作成
Pick<User, 'id' | 'name' | 'nickname' | 'lang' | 'level'>
このようにして以下のようなageというプロパティを除外した型を作成しています。
{
id: number;
name: string;
nickname: string;
lang: 'Ja' | 'En';
level: number;
};
Omitの問題点&改善案
このOmit
なんですが、1つ問題があります。
それはOmit
の第二型引数であるK
の制約が緩いことです。もう一度Omit
の実装を見てみましょう。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
型引数K
に対してextendsを使い型制約を加えているのですが、keyof any
を使っており、制約がゆるくなっています。
keyof anyは下記画像のようになり、オブジェクトや配列以外であればだいたい受け入れてくれます。
これによってどういう問題が生じるかというと、プロパティにない文字列を2つ目の引数に指定してもエラーにならないんですね。
例えば、Userのプロパティにないaaaa
みたいな適当な文字列を指定してもエラーになりません。
これだとタイプミスに気づかない恐れがありますし、何よりエディターの補完が効かないのが辛いです。
そこで改善案を考えました。それがこちらです。
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
keyof any
だった部分をkeyof T
に変えています。こうすることで2つ目の型引数には、1つ目の型引数のプロパティの文字列しか指定できなくなります。
Userのプロパティにないaaaa
を指定するとエラーが出るようになっています。
OmitStrict?
Omitの問題点を解消する改善案を考えてみましたが、keyof any
をkeyof T
を変えるだけというとてもシンプルな修正だったので、他にも同じことを考えている人がいるのではないか、TypeScript側も気付いているのではないかと考えました。
そしてgithubのissueを見てみたところ、やはりありました。それがこちらです。
このissueで議論されていて初めて知ったのですが、Omitというユーティリティタイプを追加して欲しいという依頼があった時はkeyof any
ではなく、型制約が今より厳しかったそうです。それが何故かTypeScript開発チームが実際にOmit
を追加した時(PR)にはkeyof any
になっていた。それだと第二型引数の制約が緩いのでもう少し厳しくしてくれというのがこのissueでの主張ですね。
この意見には多くの人が賛同していました。しかし、それに対するTypeScriptの中の人の回答がこちらです。
It seems like the constrained Omit type is going to make at least half of users unhappy based on declarations within DefinitelyTyped. We've decided to go with the more permissive built-in which your own constraints can build on.
(意訳: そんなことすると半分以上のユーザーがアンハッピーになる。自分で厳しめの型作れるようにライブラリのOmit
は緩めにしとくわ)
これに対して色んな人が反対意見を投稿していました。例えば以下のような感じです。
- もう半分のユーザーのことも考えてくれ!!
- どうしてPickは厳しいのにOmitはゆるいんだ!!
- Omitは残したままで良いからOmitStrictを作ってくれ!!
- 型の制約が厳しい方が良い!!
- 上位のライブラリtype-festとtypicalどちらもプロパティだけを指定できるようになっている!!
- LooseOmitとStrictOmit両方作ってどっちを使うか選ばせれば良いのでは?
これだけ否定的な意見があるにも関わらず、TypeScript側は屈せず最終的にこのように答えております。
(長いのでリンク貼りました)意訳すると、Omitの制約厳しい版は作りません。厳しさの定義は人それぞれ違うし、すでにOmitStrictという名前で独自に作っているユーザーもいる。ライブラリ側でOmitStrict
を作ると名前が衝突して良くない。といった感じのことを言っていました。
これに対して30件ほどバッドマークはついていましたが、TypeScript側としてはOmitは今のままで、制約が厳しいバージョンは作らないということです。
というわけで僕が指摘した問題点にはTypeScript側も気付いていて、あえてこうしているようです。
まとめ
今回はTypeScriptのユーティリティタイプの1つであるOmit
について見ていきました。その中で問題点を発見し、その問題点は深い議論が実施されたうえであえて残されているということが分かりました。
改善するのは簡単だけど、既存ユーザーのことを考えると変更できないというライブラリ開発者の苦労を少し感じられたような気がします。
そして今回Omit
以外にもユーティリティタイプの実装を見たことで、TypeScriptについてより詳しくなれたかなと思います。他のいくつかのユーティリティタイプの実装も共有しますので見てみてください。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type Exclude<T, U> = T extends U ? never : T;
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
Discussion