208
114

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScriptで最低一つは必須なオプションオブジェクトの型を作る

Last updated at Posted at 2018-12-28

JavaScriptにおいては、関数等がさまざまなオプションを受け取る場合にそれらをオブジェクトとして受け取ることがよく行われます。例えばよく見る所ではDOMのaddEventListenerなどが例として挙げられます。

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型を使えば楽勝?

まあ、これは難しい話ではありません。簡単な例として、foobarという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;
}

opt1opt2を見て分かるように、どのオプションも省略可能となっています。ただし、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つ目の型引数明示的に指定することでいずれか一つを必須にする範囲を狭める機能をつけるという副次的効果もあります。例えば、Optionsfooプロパティとbarプロパティのどちらか1つを指定しなければいけない場合はRequireOne<Options, 'foo' | 'bar'>と書けます。

まとめ

今回は、比較的実用に近そうな例を用いてTypeScriptの条件型の使用例を示しました。

TypeScriptは、このようにオブジェクトの型をいじるのが結構得意です。
皆さんもぜひ妥協せずにTypeScriptで型をつけていきましょう。

208
114
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
208
114

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?