JavaScriptにおいては、関数等がさまざまなオプションを受け取る場合にそれらをオブジェクトとして受け取ることがよく行われます。例えばよく見る所ではDOMのaddEventListenerなどが例として挙げられます。
document.addEventListener('touchend', handler, {
once: false,
passive: true,
});
この例では第3引数でオプションたちをオブジェクトにまとめて渡しています。
オプションたちは往々にして省略可能です。
さて、TypeScriptでもこのようなオブジェクトに型をつけることは容易です。TypeScriptにはオブジェクトの省略可能なプロパティを意味する?
が備わっているからです。
interface Options {
capture?: boolean;
once?: boolean;
passive?: boolean;
}
この例では、Options
型の3つのプロパティは全て省略可能です。そのため、{capture: false, once: true, passive: false}
や{passive: true}
、あるいは{}
などは全てOptions
型がつきます。
この記事ではもう少し複雑な場合を考えます。オプションのうち、「どれか一つは必須でほかは省略可能」という場合を考えてみましょう。上の例では、{capture: true}
や{once: true, passive: false}
などは許すが{}
は許さないということになります。この記事では、このような型を作ることを目標とします。
union型を使えば楽勝?
まあ、これは難しい話ではありません。簡単な例として、foo
とbar
という2つの省略可能なオプションがある場合を考えます。この場合、union型を用いて次のようにすれば解決です。
type Options =
| {
foo: string;
bar?: number;
}
| {
foo?: string;
bar: number;
}
すなわち、foo
が指定された場合とbar
が指定された場合の両方の型を用意してそれらのunionをとればいいのです。
しかし、見ればわかるとおりこれは明らかに理想的な解決策ではありません。foo
の型やbar
の型が2回書かれています。
理想的には、以下のような1つの型定義から上のunion型を生成したいですよね。
interface Options {
foo?: string;
bar?: number;
}
そこで、TypeScriptの機能を駆使してこれを実現するのがこの記事の内容です。
以下の内容が何を言っているのか分からない場合、筆者による以下の記事が参考になるかもしれません。
逆に、以下の記事の内容を用いて自分で解決策を作ることができれば初級~中級程度のTypeScript力があると言えると思います。
TypeScript的解決策
では、早速ですが答えを見ましょう。
type RequireOne<T, K extends keyof T = keyof T> =
K extends keyof T ? PartialRequire<T, K> : never;
type PartialRequire<O, K extends keyof O> = {
[P in K]-?: O[P]
} & O;
ここで定義したRequireOne
が今回欲しかったものです。これは以下のように使います。
type Options = RequireOne<{
foo?: number;
bar?: number;
baz?: string;
}>
const opt1: Options = {foo: 3};
const opt2: Options = {bar: 5, baz: "foo"};
const opt3: Options = {foo: 123, bar: 123, baz: "foo"};
// エラー
const opt4: Options = {};
/** オプションを受け取る関数の例 */
function useOptions(opts: Options) {
const {
foo = 0, // number型
bar = 100, // number型
baz, // string | undefined型
} = opts;
}
opt1
やopt2
を見て分かるように、どのオプションも省略可能となっています。ただし、opt4
のように全部省略されているとエラーとなります。
解説
まずPartialRequire
を解説します。
type PartialRequire<O, K extends keyof O> = {
[P in K]-?: O[P]
} & O;
これは、オブジェクトの型のうち一部のみを省略不可とした型を返すものです。
どのフィールドを省略可能とするかをK
で指定します。
例えば、上の例のOptions
に対してPartialRequire<Options, 'foo'>
とした場合はfoo
のみが省略不可能になった型、すなわち{foo: number; bar?: number; baz?: string}
(と同等の型)が返されます。
定義を見ると、&
の前が省略不可能部分の定義となっており、-?
で省略可能性を取り除いているのがポイントです。PartialRequire<Options, 'foo'>
の場合はここが{foo: number}
となります。それにO
をそのままくっつけることで残りの部分を維持しています。
これを用いると、Options
から生成すべき型はPartialRequire<Options, 'foo'> | PartialRequire<Options, 'bar'> | PartialRequire<Options, 'baz'>
となります。
これを作るのがRequireOne
です。
type RequireOne<T, K extends keyof T = keyof T> =
K extends keyof T ? PartialRequire<T, K> : never;
これはRequireOne<Options>
として使うとまず型引数K
がデフォルト値のkeyof T
となります。今回の場合はkeyof Options
なので'foo' | 'bar' | 'baz'
というunion型になります。
こうなると、union型の各要素を'foo'
→ 'PartialRequire<Options, 'foo'>
のように変換すれば目的が達成できそうですね。
これは実は、条件型のunion distributionという性質を使えばまさにやりたいことがそのまま実化できます。K extends ***
という形の条件型を書くことで、K
の中のunionが分解されてPartialRequire<T, *>
のunionに変換されます。
なお、RequireOne
の型引数を2つにしているのは、keyof T
を型変数に入れることでunion distributionを発生させるという目的が一つありますが、2つ目の型引数明示的に指定することでいずれか一つを必須にする範囲を狭める機能をつけるという副次的効果もあります。例えば、Options
のfoo
プロパティとbar
プロパティのどちらか1つを指定しなければいけない場合はRequireOne<Options, 'foo' | 'bar'>
と書けます。
まとめ
今回は、比較的実用に近そうな例を用いてTypeScriptの条件型の使用例を示しました。
TypeScriptは、このようにオブジェクトの型をいじるのが結構得意です。
皆さんもぜひ妥協せずにTypeScriptで型をつけていきましょう。