📖

TypeScriptで引数によって戻り値の型を変えてみた【条件付き型 × ジェネリクス】

に公開

本記事では、条件付き型(Conditional Types) と ジェネリクス を使って引数の値に応じて返す型を変える方法を紹介します。

やりたいこと

例えば "dog" という引数が渡されたら Dog 型を返し、"cat" の場合は Cat 型を返すような関数を作りたい。

条件付き型で定義する

まずは型を定義します。

type Dog = { kind: "dog"; bark(): void };
type Cat = { kind: "cat"; meow(): void };

export type AnimalType<T> = T extends "dog"
  ? Dog
  : T extends "cat"
  ? Cat
  : never;

ここでは T が "dog" なら Dog、"cat" なら Cat を返すという条件付き型を定義しています。

ジェネリクス関数で使う

function createAnimal<T extends "dog" | "cat">(
  type: T
): AnimalType<T> {
  if (type === "dog") {
    return { kind: "dog", bark: () => console.log("woof") } as AnimalType<T>;
  } else if (type === "cat") {
    return { kind: "cat", meow: () => console.log("meow") } as AnimalType<T>;
  } else {
    throw new Error("Unknown animal type");
  }
}

<T extends "dog" | "cat"> のように型パラメータを導入し、AnimalType<T> を戻り値の型に指定して、引数 type に応じた返り値の型を返します。

呼び出す

const dog = createAnimal("dog");
dog.bark(); // OK
dog.meow(); // ❌ エラーになる

const cat = createAnimal("cat");
cat.meow(); // OK
cat.bark(); // ❌ エラーになる

どんな時に使える?

  • APIの種類やイベントの種類ごとに、返却される型が異なるとき
  • フォームや入力の種類によって送信データの構造が変わるようなとき
  • コードの安全性を保ちながら共通関数にロジックを集約したいとき

利用を避けたほうがいいケース

  • 型の分岐が非常に多く、読みづらくなる場合(2〜3つくらいがちょうど良いと思う)
  • 返却される型が複雑で、関数の中で条件ごとの実装を維持するのが困難なとき
  • チームにジェネリクスや条件付き型に不慣れなメンバーが多いとき

まとめ

  • T extends A ? X : Y のように 動的に型を切り替えるテクニックは、柔軟で型安全
  • 実務ではAPIの種類やイベントの種類などに応じて、返り値の型を切り替えるのに便利
  • ただし、使いすぎるとコードが読みにくくなるため、バランスよく使おう

ちょっとニッチなテーマでしたが、読んでくださりありがとうございました。

NCDCエンジニアブログ

Discussion