LoginSignup
1
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-01-26

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

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

動的にレスポンスが変わる 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許容型) にするのではないでしょうか?

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

問題点

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

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

解決策

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

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

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

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

// 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 型が正しく動作するか検証してみます。

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

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

配列の中身によって動的に型が切り替わる: その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)
配列の中身によって動的に型が切り替わる: その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 呼び出しもこのようになるでしょう。

改良
+ 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:

1
2
2

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
1
2