普段は Union 型が返るけど、文字列で種類を渡したら具体的な型で返ってくる関数を作る

※ 本記事の内容は最終的に `as` で回避している箇所があります。たぶんどうしようもないと思いますが、より良い方法をご存知の方、思いつく方は是非コメントください（無理だと思う、というコメントも歓迎します）。

# やりたいこと

TypeScript で以下のような判別共用体を使うと思います。

```ts
interface Hoge {
    type: "Hoge";
    foo: string;
}

interface Fuga {
    type: "Fuga";
    bar: string;
}

type Piyo = Hoge | Fuga;
```

これに対して、引数で `"Hoge"` や `"Fuga"` を渡すとそれぞれのインスタンスを返してくれ、引数を省略する（`undefined` を渡す）と `Hoge` か `Fuga` のどちらかを返すような関数があったとします。この関数の戻り値を単純に `Piyo` 型（つまり `Hoge | Fuga` 型）とすると、いささか不便です。

```ts
// Piyo["type"] は "Hoge" | "Fuga" 型と同義
type PiyoType = Piyo["type"];
function getPiyo(type?: PiyoType): Hoge | Fuga {
    // (実装略)
}

const a = getPiyo("Hoge"); // かならず Hoge が返る
console.log(a.foo);
              ~~~ Property 'foo' does not exist on type 'Piyo'. Property 'foo' does not exist on type 'Fuga'.

const b = getPiyo("Fuga"); // かならず Fuga が返る
console.log(b.bar);
              ~~~ Property 'bar' does not exist on type 'Piyo'. Property 'bar' does not exist on type 'Hoge'.

const c = getPiyo(); // Hoge か Fuga のどちらかが返る
if (c.type === "Hoge") { // なので当然、型ガードが必要
    console.log(c.foo); // 分岐した中なので呼べる。TypeScript コンパイラ賢い
} else {
    console.log(c.bar);
}
```

イメージとしては、`document.querySelector` に `document.querySelector(".foo")` とすると `Element` が返るけど、`document.querySelector("button")` とすると `HTMLButtonElement` が返る[^1]のと似たようなことがやりたいです。

すなわち、先ほどのエラーの箇所が `if` による**分岐なく** `Hoge` や `Fuga` として、**キャスト無しで**プロパティアクセス出来てほしい。どうやるの？というのが当記事の内容です。

ちなみに、以下の条件で実現したいです。

* 今回はわけあって[^2]オーバーロードを使わない。
* Hoge や Fuga の名前を直接随所に書きたくない。Piyo を使って定義したい（後から増えていくため）。

# やり方

結果的に以下のやり方を思いつきました。

```ts
// T には extends PiyoType 制約をつけてもよいかも
type PiyoOf<T, P extends Piyo = Piyo> = P extends { type: T } ? P : never;
type PiyoTypeMap = { [T in PiyoType]: PiyoOf<T> };

function <T extends PiyoType>(type?: T):
        T extends undefined ? Piyo : PiyoTypeMap[T] {
    // (実装略)
    return piyo as T extends undefined ? Piyo : PiyoTypeMap[T]; // ← ここで as しないと互換性がないと怒られる
}
```

※ ちなみに `as` は外せなくて、中で `if (type === undefined)` などで分岐してそのパスで `return` しても整合性が取れてるとは考えてくれないようです。`as` を使うと `{}` なども返せてしまいましたが、`null` や `undefined`、`0` や　`""` などは弾いてくれたので `as any` よりは幾分マシかと[^3]。

これをもうちょっと省略すると

```ts
function <T extends PiyoType, R extends Piyo>(type?: T):
        T extends undefined ? Piyo : (R extends { type: T } ? R : never)
```

が、投稿後に[参考記事で挙げている演習](https://qiita.com/uhyo/items/e4f54ef3b87afdd65546#4-6-%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E6%8F%8F%E7%94%BB%E3%81%99%E3%82%8B%E9%96%A2%E6%95%B0)にそっくりであることに気づいて、以下がベストという結論になりました。「これ型演習でやったところだ！」ってなるのが遅い。。。

```ts
function getPiyo<T extends PiyoType>(type?: T):
        undefined extends T ? Piyo : Extract<Piyo, { type: T }>
}
```

[Playground で試す](https://www.typescriptlang.org/play/#code/PTAEFp1Qs7USv9BcFC4BQBLAdgFwKYCcBmAhgMbagASA9gOZkDeyoomAngA7YBcoARFbTwDcjUPkqVuAZ0y4M1YQF9kaLHiKlQAMQCu1QqAZNWHbjx16hIgEaFcUmXMXLjZAAqoWlUAF4KNMgA+WrqEwi6g7p4AKuxkvpGUANo8LjwAuoKgILz82DygQWYh+YAWDIAwKrDK+NroxJiolOigtJgJADxRoNgAHjjoACaSER6UMRwAfAAUIi4A-NxRyACU3DX92PgY2P1dvdgDQ52zw56g3ACivbgkmG0JADQGzLELoArjBspM2YC1DIDHDIBOhkAEwyAJIZAH4MJUAgQyAMwZAB9mgHkGQD6DJDANYMgFuGQA-DIBqhkA1wyAItTAA6mKMA+dqAUYjwVDANEMImIjWkoFQAFs2AAbHygSYubgJMaBUBrDZbforE5ebwfQxMUAiJiofAc8LeJX8gaC9DbJafKXS7VMWnoenEdkuYS60C4bCYbS4JoAWUImAAFgA6a4DSiMyaatqgAAMzoArDLdcc6M8TDl-DxHmIJLxY-klGamNww1zeOZCNHQDY7Lxc4nhMGFF0WZIyHKFbEfMq+FHNZKmMGmBarTanum6wIY+JTAm3qapcXS+WGfLOdWlb4ihYG8Hm+bLdammmXhnio9c6YCwP59qk1LW8vw9hMtl1QA3PAiBRF75gf7AwDaDIAtBhRgBX4wC6DIAxBhKgE0GSkSkAJANAEiGQAYhkpSZK0AcNNADsGQBaOUAcwZFzbJpAHUGaFACCGMpAA3LJYRCPdsmVZCcOE1QghkmAVNnVHYej6QZQCOUUzlAS4ZBuO4RkeVcI06d4licZBskgUBAEolQAKV0QSBkH1elHX8dkWgSSYuzyQTZLpSgWWwZ0WRoSYFNoZ1Yw0zSDUwUQQiUy0VJnLMNLk7TdP06hJmqPRnVzMy5MstgRhs1oRi9YQnJ0vSDP8zxnRcDSgA)

## document.querySelector を参考に PiyoMap を作るやり方

`lib.dom.d.ts` を見てみると `document.querySelector` は以下のようになっています。

```ts
querySelector<K extends keyof HTMLElementTagNameMap>(selectors: K): HTMLElementTagNameMap[K] | null;
```

ほうほう、`HTMLElementTagNameMap` とな…？見てみます。

```ts
interface HTMLElementTagNameMap {
    "a": HTMLAnchorElement;
    "abbr": HTMLElement;
    "address": HTMLElement;
    "applet": HTMLAppletElement;
    ...
}
```
あー、なるほど…

つまり、与えられた文字列から返す型へマップする辞書型を定義しておいて、それを `Map[K]` として型変数をキーに戻り値の型を取り出すと。

ということで、先の `Piyo` および `PiyoType` から以下のような型を作ることを考えてみます。

```ts
type PiyoMap = {
    "Hoge": Hoge,
    "Fuga": Fuga
}
```

いま、`"Hoge" | "Fuga"` は手元にあるので、Mapped type が使えそうです。

```ts
type PiyoMap = { [T in PiyoType]: ...(ここに T が "Hoge" なら Hoge を、"Fuga" なら Fuga をもってきたい)... }
```

`なら` という単語から Conditional type が使えそうとにらみます。上記コメントでは具体的な言い方をしましたが、やりたいことをより一般化すると「`{ type: T }` であるような `Piyo` の要素にしたい」です。

Conditional type での条件といえば `extends` によるものですから、`Piyo` の中から **`{ type: T }` であるような**ものだけ残せばいいと考えます。`Piyo` が Union type であることを思い出しましょう。

Union type からある条件を満たすものだけ残して他がなくなったような型を作るには Union distribution を利用します。

**(投稿後追記): 以下の `PiyoOf<K, P>` は標準型の `Extract<T, U>` を使って `Extract<Piyo, { type: T }>` と書けそうでした**

```ts
PiyoOf<K, P extends Piyo> = P extends { type: K } ? P : never
```

Union distribution を発生させるために `Piyo`　を型変数に入れます。`P extends Piyo` という制約がある型変数ですが、実は Union distribution を発生させたいだけなので `Piyo` しか指定されません。

試しに `K = "Hoge"` として考えてみます。

```ts
PiyoOf<"Hoge", Piyo>
```

となった場合

```ts
P(= Piyo = Hoge | Fuga) extends { type: "Hoge" } ? P : never
```

となります（括弧書きは私のイメージです）。

`P` が `extends` の前に単独で存在する型変数で、かつ `P` に `Piyo` つまり Union 型が指定されたため、 Union distribution が起こります。

```ts
(Hoge extends { type: "Hoge" } ? Hoge : never) | (Fuga extends { type: "Hoge" } ? Fuga : never)
```

ここで左は条件を満たし、右は条件を満たさないので

```ts
Hoge | never
```

となり、「Union における `never` は無いものとして振る舞う」ため

```ts
Hoge
```

となります。

したがって

```ts
type PiyoOf<T, P extends Piyo = Piyo> = P extends { type: T } ? P : never;
type PiyoTypeMap = { [T in PiyoType]: PiyoOf<T> };
```

の `PiyoTypeMap` は

```ts
type PiyoTypeMap = {
    "Hoge": PiyoOf<"Hoge">,
    "Fuga": PiyoOf<"Fuga">
}
```

となり、先ほどの説明により

```ts
type PiyoTypeMap = {
    "Hoge": Hoge,
    "Fuga": Fuga
}
```

となります。

あとは目的の関数で引数が `undefined` かどうかで分岐して `PiyoTypeMap` を使えば OK です。

```ts
type PiyoOf<T, P extends Piyo = Piyo> = P extends { type: T } ? P : never;
type PiyoTypeMap = { [T in PiyoType]: PiyoOf<T> };

return function <T extends PiyoType>(type?: T):
    // T つまり引数の型が undefined なら Piyo(=Hoge|Fuga)、そうじゃなければ PiyoTypeMap[T] つまり T に応じた具象型
        T extends undefined ? Piyo : PiyoTypeMap[T] {
    // (実装略)
    return piyo as any; // ← ここで as any しないと互換性がないと怒られる
}
```

## PiyoTypeMap を介さずに直接 Union distribution を使うやり方

さらに省略した方は何をやっていたでしょうか？

```ts
function <T extends PiyoType, R extends Piyo>(type?: T):
    T extends undefined ? Piyo : (R extends { type: T } ? R : never);
```

ここで

```ts
T extends undefined ? Piyo : 
```

はまぁいいですね。引数が `undefined` なら Union 型である `Piyo` で返る（＝Hoge | Fuga という未確定状態）ということです。

else 節の方は何かというと

```ts
(R extends { type: T } ? R : never)
```

`R` は型変数ですが推論に任せる想定です。

この関数を呼ぶ側としては

```ts
getPiyo("Hoge");
```

のように呼ぶので、`T` は `"Hoge"` などに推論されます。

そして `R` は `Hoge` または `Fuga` なわけなので `Hoge | Fuga` として条件判定が入ります（たぶん…ここら辺あやしい[^4]）。すると先ほどと同じく Union distribution によって `(R extends { type: T } ? R : never)` は条件型の Union 型に分配されます。

`T` は `"Hoge"` と推論されてますから

```ts
(Hoge extends { type: "Hoge" } ? Hoge : never) | (Fuga extends { type: "Fuga" } ? Fuga : never)
```

と先ほど見たような形になり、最終的に `R` は `Hoge` に落ち着く、となります。

## そもそも演習でやったところだ

やってる最中に若干のデジャヴを感じつつも気づかなかったのですが、[TypeScriptの型演習 4-6](https://qiita.com/uhyo/items/e4f54ef3b87afdd65546#4-6-%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E6%8F%8F%E7%94%BB%E3%81%99%E3%82%8B%E9%96%A2%E6%95%B0)にそっくりでした。それによると `Extract<T, U>` がまさに欲しかったもので、自前で定義した `PiyoOf<T, P>` とほぼ一緒です。

また、`T extends undefined` より `undefined extends T` の方が良かったと思う（Union distribution に気を使わなくていいから？継承関係的に？今回はどっちでもいい気もする？）ので、そこも直すと以下のようにできます。

```ts
function getPiyo<T extends PiyoType>(type?: T):
    undefined extends T ? Piyo : Extract<Piyo, { type: T }>
```

非常にシンプルですね。

## 感想

最後に感想として、`X extends Y ? X : never` という形は常套句（イディオム）っぽいなぁと感じました。

Union 型を型世界でいう配列として捉えたとき、実行コードでいう `.filter(x => x === y)` に近い意味合い（実際は `extends` なので `x is Y` みたいな？）と考えるとイメージしやすいかと思います。もにゃどでいうと `.flatMap(x => x === y ? [x] : [])` ってことかと。

解説は以上となります。

# 参考記事

当記事を書くまでの理解度に到達するにあたって、下記記事には大変お世話になりました。こちらでお礼を述べさせていただきます。

- [TypeScriptの型入門](https://qiita.com/uhyo/items/e2fdef2d3236b9bfe74a)
- [TypeScriptの型初級](https://qiita.com/uhyo/items/da21e2b3c10c8a03952f)
- [TypeScriptの型演習](https://qiita.com/uhyo/items/e4f54ef3b87afdd65546)

[^1]: 実際にはセレクタにマッチしないケースがあるので `| null` になりますが本題ではないのでおいときます
[^2]: クロージャーとして返す関数で実現したかったからなのですが、もしかして `return function(...){ ... }` で返す関数に引数違いのオーバーロード定義できたりしますかね…
[^3]: プリミティブ型が弾かれるのに `{}` はキャストできてしまう理屈がよくわからない…オブジェクト型かプリミティブ型かのレベルの判定だけ有効ってこと…？
[^4]: Conditional type の結果部は条件部を判定するまで遅延評価されます。最終的に `R` は `Hoge` に確定するのですが、その前に条件を評価する必要があり、その時点ではまだ `R` は `Piyo` であることまでしか推論できないので Union distribution が発生します。という理屈だと思います。。。