TypeScriptで「最低一つは必須なオプションオブジェクトの型」を読みやすくしてみる

## 序

**「弱い TypeScripter」は、「強い型付け」に惹かれつつもそれを自分で作ることはできない。**

それは能力的な制約だったり時間的な制約だったりするのだが、「集合知」というものは偉大なため、問題を自分で型付けられなかったとしても Qiita などの技術記事から「強い TypeScripter」の叡智に与ることができる。

例えば「オブジェクトのいくつかあるプロパティのうち、最低一つは必須であるような型」を付けたいときは、@uhyo 氏の[『TypeScriptで最低一つは必須なオプションオブジェクトの型を作る』](https://qiita.com/uhyo/items/583ddf7af3b489d5e8e9)からコピペして[^1]使えばいい。

[^1]: 権利的に心配なコードをコピペするときはライセンスをよく読もう。私は私のあとにコードを触る者が「[極北のTypeScripter](https://speakerdeck.com/uhyo/an-quan-xing-falseji-bei-karajian-rutypescript)」の影響を受けてほしい（一度は理想論⊆極論に触れてほしい）ので引用元を示すためにコード内のコメントで記事へのリンクを貼っている。

```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;
```

もちろん「コピペして使えばいい」とは言っても、コードの中身を理解せずに貼り付けるのはなかなか怖いものがある。

そんなときは**ちゃんと解説を読んで理解する**......ことが出来れば問題ないわけだが、TypeScripter として弱いとどうしても理解が曖昧になったりしてしまう。

そうなると実際に[**型関数に対してテストを書い**](https://qiita.com/u-sho/private/93b56f3528dabc35f1fb)てみて挙動を見てみるしかない。

```typescript:generics.test.ts
import type { RequireOne } from 'lib/types/generics';

type BaseOptions = {
    foo?: number,
    bar?: number,
    baz?: string,
};

test('RequireOne<T>', () => {
    // One of 'foo', 'bar', 'baz' is required at least.
    type Options = RequireOne<BaseOptions>;

    // @ts-expect-error
    const opt0 : Options = {};
    const opt11: Options = { foo: 3 };
    const opt12: Options = { bar: 3 };
    const opt13: Options = { baz: '' };
    const opt21: Options = {         bar: 5, baz: 'foo' };
    const opt22: Options = { foo: 5,         baz: 'foo' };
    const opt23: Options = { foo: 5, bar: 2,            };
    const opt3 : Options = { foo: 1, bar: 3, baz: 'foo' };
});

test('RequireOne<T, K>', () => {
    // One of 'foo', 'bar' is required at least
    type NumOptions = RequiredOne<BaseOptions, 'foo' | 'bar'>;

    // @ts-expect-error
    const opt0 : NumOptions = {};
    const opt11: NumOptions = { foo: 3 };
    const opt12: NumOptions = { bar: 3 };
    // @ts-expect-error
    const opt13: NumOptions = { baz: '' };
    const opt21: NumOptions = {         bar: 5, baz: 'foo' };
    const opt22: NumOptions = { foo: 5,         baz: 'foo' };
    const opt23: NumOptions = { foo: 5, bar: 2,            };
    const opt3 : NumOptions = { foo: 1, bar: 1, baz: 'foo' };
});
```

なんとなくではあるものの、ある程度網羅的に使用例に触れて挙動が理解できた気がする。テストもできたし、StoryBook 的な使用例もついてくる。

**完璧だ。これで安心してコピペできる！**

おっと、`foo` や `bar` に `string` 型を代入することは可能だろうか？

```typescript:requireOne.test.ts
import type { RequireOne } from 'lib/types/generics';

type BaseOptions = {
    foo?: number,
    bar?: number,
    baz?: string,
};

test('RequireOne<T>', () => {
    // One of 'foo', 'bar', 'baz' is required at least.
    type Options = RequireOne<BaseOptions>;

    // @ts-expect-error
    const opt0 : Options = {};
    const opt11: Options = { foo: 3 };
    const opt12: Options = { bar: 3 };
    const opt13: Options = { baz: '' };
    const opt21: Options = {         bar: 5, baz: 'foo' };
    const opt22: Options = { foo: 5,         baz: 'foo' };
    const opt23: Options = { foo: 5, bar: 2,            };
    const opt3 : Options = { foo: 1, bar: 3, baz: 'foo' };

    // @ts-expect-error
    const omg11: Options = { foo: 'str' };
    // @ts-expect-error
    const omg12: Options = { bar: 'str' };
    // @ts-expect-error
    const omg13: Options = { baz: 3 };
    // @ts-expect-error
    const omg3 : Options = { who: 1, bar: 3, baz: 'who?' };
});

test('RequireOne<T, K>', () => {
    // One of 'foo', 'bar' is required at least
    type NumOptions = RequireOne<BaseOptions, 'foo' | 'bar'>;

    // @ts-expect-error
    const opt0 : NumOptions = {};
    const opt11: NumOptions = { foo: 3 };
    const opt12: NumOptions = { bar: 3 };
    // @ts-expect-error
    const opt13: NumOptions = { baz: '' };
    const opt21: NumOptions = {         bar: 5, baz: 'foo' };
    const opt22: NumOptions = { foo: 5,         baz: 'foo' };
    const opt23: NumOptions = { foo: 5, bar: 2,            };
    const opt3 : NumOptions = { foo: 1, bar: 1, baz: 'foo' };

    // @ts-expect-error
    const omg11: NumOptions = { foo: 'str' };
    // @ts-expect-error
    const omg12: NumOptions = { bar: 'str' };
    // @ts-expect-error
    const omg21: NumOptions = { foo: 3,         baz: 3 };
    // @ts-expect-error
    const omg22: NumOptions = {         bar: 3, baz: 0 };
    // @ts-expect-error
    const omg3 : NumOptions = { who: 1, bar: 3, baz: 'who?' };
});
```

・・・・・・ふぅ。

このくらいでいいだろうか。

しかし、こうなってくると他に考慮漏れはないか不安になってくる。ブラックボックステストでは限界がある。

そもそも元の型定義さえサラッと読めればこんなことに悩まなくてもいいのに（嘘

そう、元の型定義さえサラッと読めれば――

そうか、**リファクタリング**だ！

## 本論

さて、もとのコードを思い出そう。

```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;
```

簡単なことからしていこう。

まずは説明変数的に見える `PartialRequire` の中身を `RequireOne` に直接書いてみる。

```typescript
type RequireOne<T, K extends keyof T = keyof T> =
    K extends keyof T
    ? { [P in K]-?: T[P] } & T
    : never;
```

これで動くだろうか。

テストはすでに書いていた[^tdd]ので、結果を見てみよう。

[^tdd]: **リファクタリングをするときはまずテストを書け**ということが　Kent Beck 著, 和田 卓人 訳『[テスト駆動開発](https://shop.ohmsha.co.jp/shopdetail/000000004967/)』オーム社　や、　David Scott Bernstein 著, 吉羽 龍太郎, 永瀬 美穂, 田 騎郎, 有野 雅士 訳『[レガシーコードからの脱却 ―ソフトウェアの寿命を延ばし価値を高める9つのプラクティス](https://www.oreilly.co.jp/books/9784873118864/)』O'Reilly Japan、　Martin Fowler 著, 児玉 公信, 友野 晶夫, 平澤 章, 梅澤 真史 訳『[リファクタリング 既存のコードを安全に改善する（第2版）](https://www.ohmsha.co.jp/book/9784274224546/)』オーム社　などに書かれている。鉄則である。

・・・・・・うむ、大丈夫なようだ。

次は `K extends keyof T` が2回出てきているので、片方を削れないか試してみよう。

（私は Union Distribution[^union-distribution] を知っているので、型引数から削る）

[^union-distribution]: Union Distribution については『[TypeScriptの型初級#conditional-typeにおけるunion-distribution](https://qiita.com/uhyo/items/da21e2b3c10c8a03952f#conditional-type%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8Bunion-distribution)』に詳しく書いてある。公式ドキュメントはこちら「[TypeScript: Documentation - TypeScript 2.8 #Distributive Conditional Types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types)」

```typescript
type RequireOne<T, K = keyof T> =
    K extends keyof T
    ? { [P in K]-?: T[P] } & T
    : never;
```

テストも大丈夫なようだ。

だいぶスッキリしてきた。

簡単な整理は終わったので、次は難しいところに移ろう。

```typescript
{ [P in K]-?: T[P] }
```

`T` のうち key が `K` に含まれているものを抜きだして[^mapped-type-in]さらに required (必須) にしている[^mapped-type-require]。

[^mapped-type-in]: `[P in K]: T[P]` のような `in` の使い方に関しては『[TypeScript の"is"と"in"を理解する#in](https://qiita.com/ryo2132/items/ce9e13899e45dcfaff9b#in)』を参照。TypeScript 公式はこちら「[TypeScript: Documentaion - Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html)」

[^mapped-type-require]: `key?: value` で `key` を optional にできるように、mapped types についても `{[P in keyof T]?: T[P]}` で `Partial<T>` と同じ型が得られる。また `{[P in keyof T]-?: T[P]}` で `Required<T>` と同じ型となる。これは mapping modifier と呼ばれる機能で `?` と `readonly` に対して使える。「[TypeScript: Documentaion - Mapped Types](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers)」

ここで Utility Types[^utility-types] を思い出すと、「`T` のうち key が `K` に含まれているものを抜きだ」すのは  `Pick<T,K>` と同じ動作であるし、「required (必須) に」するのは `Required` と同じことである。

[^utility-types]: Utility Types なんて覚えてないよという読者は『[【TypeScript】Utility Typesをまとめて理解する](https://qiita.com/k-penguin-sato/items/e2791d7a57e96f6144e5)』が簡潔にまとまっているのでブックマークしておくといい。公式ドキュメントは「[TypeScript: Documentation - Utility Types](https://www.typescriptlang.org/docs/handbook/utility-types.html)」にある。

もしかしてこう書けるのではないだろうか。

```typescript
type RequireOne<T, K = keyof T> =
    K extends keyof T
    ? Required<Pick<T, K>> & T
    : never;
```

テストの結果を見ると大丈夫なようだ。

かなり分かりやすくなった。

個人的には

```typescript
Required<Pick<T, K>> & T
```

よりも

```typescript
Required<Pick<T, K>> & Omit<T, K>
```

のほうが理解しやすいがどうだろうか。

型引数の制限はあったほうがいいだろうから戻しておこう。

```typescript
type RequireOne<T, K extends keyof T = keyof T> =
    K extends keyof T ? Required<Pick<T, K>> & Omit<T, K> : never;
```

## 跋

「[最低一つは必須なオプションオブジェクトの型](https://qiita.com/uhyo/items/583ddf7af3b489d5e8e9)」について、[テストを書き](https://qiita.com/u-sho/private/93b56f3528dabc35f1fb)、リファクタリングをして、以下のように簡潔にした。

```typescript:before
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;
```

```typescript:after
type RequireOne<T, K extends keyof T = keyof T> =
    K extends keyof T
    ? Required<Pick<T, K>> & Omit<T, K> // Omit<T, K> は単に T でもよい
    : never;
```

---
蛇足だが、`T[K]` が optional な型でないときはこうすれば良い

```typescript
type RequireOne<T, K extends keyof T = keyof T> =
    K extends keyof T
    ? Required<Pick<T, K>> & Partial<Omit<T, K>>
    : never;
```