【TypeScript】配列の中身によって動的に型を切り替えたい

# どんなシーンを想定しているか

`includes[]` パラメータの有無によって動的にレスポンスが変わる Web API があったとします。

```diff_json:動的にレスポンスが変わる Web API
# リクエスト
- GET https://example.com/profile/1
+ GET https://example.com/profile/1?includes[]=age&include[]=image

# レスポンス
{
  "id": 1,
  "name": "Taro",
+ "age": 25,
+ "image": "https://example.com/images/profile/1"
}
```

このレスポンスの型を素直に書くと、以下のように Nullable （Null許容型） にするのではないでしょうか？

```typescript: 動的なレスポンスは Nullable にしてしまうと...
type Profile = {
  id: number;
  name: string;
  age?: number;
  image?: string;
}
```

# 問題点

しかし、これには問題があります。
例えば、age も image も含めた `includes` パラメータを受け取り API を実行した場合、**値が入っているにも関わらず、`undefined` の可能性があると判定されるのです。**

```typescript:undefined の可能性があると判定されてしまう
const params = {
  id: 1,
  includes: ['age', 'image']
}

const profile = await profileApi.fetch<Profile>(params); // fetch のジェネリクス型に戻り値の型を指定

console.log(profile.age > 30);  // NG
// 'profile.age' は 'undefined' の可能性があります。ts(18048)

// ↓ これならOK
console.log(profile.age && profile.age > 30);
```

# 解決策

:::note
コメント欄で @sugoroku_y さんから教えていただいたPickなどのUtility Typesを使った簡潔な手法がありますので、そちらをご覧ください:bow:
https://qiita.com/mkin/items/b0c0bfecac9e21f5fded#comment-080284e4a8a610916065

以降の記事は簡潔でないケースとして残しておきます。
:::

これを解決するためにやることは以下の2点です。

**簡潔でないケース**
~~1. ベースになる型と動的になる型を分離する~~
~~2. Conditional Type を使って交差型を作る~~

```typescript
// 1. ベースになる型と動的になる型を分離する
type ProfileBase = {
  id: number;
  name: string;
}
type AgeParam = { age: number }     // Not Nullable
type ImageParam = { image: string } // Not Nullable

// 2. Conditional Type を使って交差型を作る
type Profile<IncludesParam = ''> = ProfileBase
 & ('age' extends IncludesParam ? AgeParam : ProfileBase)
 & ('image' extends IncludesParam ? ImageParam : ProfileBase)
```

`&` を使って型を結合する（交差型を作る）と、必要な型のみ生成することができます。
配列にマッチしなければ、ベースの型を結合する（＝変化なし）ようにしています。

試しに、Profile 型が正しく動作するか検証してみます。

```typescript:配列の中身によって動的に型が切り替わる: その1
// OK
const profile: Profile = { id: 1, name: 'Taro' }
```

補足として、ジェネリクス型を `type Profile<IncludesParam = ''>` のようにデフォルト値を設定しているため、ジェネリクス型を指定する必要はありません。

```typescript:配列の中身によって動的に型が切り替わる: その2
// age のみ
const includes = ['age'] as const;
type IncludesParam = (typeof includes)[number];
// OK
const profile2A: Profile<IncludesParam> = { id: 1, name: 'Taro', age: 25 }
// NG
const profile2B: Profile<IncludesParam> = { id: 1, name: 'Taro' }
// Type '{ id: number; name: string; }' is not assignable to type 'Profile<"age">'.
// Property 'age' is missing in type '{ id: number; name: string; }' but required in type 'AgeParam'.(2322)
```

```typescript:配列の中身によって動的に型が切り替わる: その3
// age と image
const includes = ['age', 'image'] as const;
type IncludesParam = (typeof includes)[number];
// OK
const profile3c: Profile<IncludesParam> = { id: 1, name: 'Taro', age: 25, image: 'https://example.com/images/profile/1' }
// NG
const profile3a: Profile<IncludesParam> = { id: 1, name: 'Taro' }
const profile3b: Profile<IncludesParam> = { id: 1, name: 'Taro', age: 25 }
```

うまく動いているようです。


先程の Web API 呼び出しもこのようになるでしょう。

```diff_typescript:改良
+ const includes = ['age', 'image'] as const;
const params = {
  id: 1,
- includes: ['age', 'image']
+ includes 
}

- const profile = await profileApi.fetch<Profile>(params);
+ type IncludesParam = (typeof includes)[number];
+ const profile = await profileApi.fetch<Profile<IncludesParam>>(params);

console.log(profile.age > 30);  // OK
```

# おわりに

TypeScript の型をきれいに書くことで、不要な `undefined` の判定処理を無くせることが分かりました。一方で、コードの記述量が多くなり、初めて見るときに身構えてしまうかもしれません。

そんなときは、**TypeScript はあくまで JavaScript をサポートするためのもので、処理に影響を与えない**ことを意識するとよいかと思います。
つまり、`type 〜` や `〜<...>` の型部分を除外してみると、意外とコード量は多くなかったりします。

もっと良い方法がありましたら教えていただけると幸いです :bow: