ReturnType と Parameters で関数の型を抽出する

関数の戻り値や引数の型を取り出したいとき、ReturnTypeParameters が使える。既存の関数から型を導出することで、型定義の重複を避けられる。

ReturnType

ReturnType は関数型から戻り値の型を抽出する。

function createUser(name: string, age: number) {
  return {
    id: crypto.randomUUID(),
    name,
    age,
    createdAt: new Date(),
  };
}

type User = ReturnType<typeof createUser>;
// { id: string; name: string; age: number; createdAt: Date }

関数の実装から型を導出できるため、型定義を別途書く必要がない。関数の戻り値が変更されれば、型も自動的に追従する。

typeof が必要な点に注意。ReturnType は関数の「型」を受け取るため、関数そのものではなく typeof 関数名 を渡す。

// 関数型を直接渡す場合
type StringParser = (input: string) => string[];
type ParseResult = ReturnType<StringParser>;  // string[]

Parameters

Parameters は関数型から引数の型をタプルとして抽出する。

function greet(name: string, age: number, greeting?: string) {
  return `${greeting ?? "Hello"}, ${name}! You are ${age}.`;
}

type GreetParams = Parameters<typeof greet>;
// [name: string, age: number, greeting?: string]

タプル型なので、インデックスでアクセスできる。

type FirstArg = Parameters<typeof greet>[0];  // string
type SecondArg = Parameters<typeof greet>[1];  // number
ReturnType

関数の戻り値の型を抽出する。戻り値を他の関数で使いたいときに便利。

Parameters

関数の引数の型をタプルで抽出する。同じ引数を受け取るラッパー関数を作るときに便利。

実践例:ラッパー関数

元の関数と同じ引数を受け取るラッパー関数を作るとき、Parameters が役立つ。

function fetchUser(id: string, options?: { cache: boolean }) {
  // 実装
}

function fetchUserWithRetry(
  ...args: Parameters<typeof fetchUser>
): ReturnType<typeof fetchUser> {
  for (let i = 0; i < 3; i++) {
    try {
      return fetchUser(...args);
    } catch (e) {
      if (i === 2) throw e;
    }
  }
  throw new Error("Unreachable");
}

元の関数のシグネチャが変わっても、ラッパー関数は自動的に追従する。

実践例:API クライアント

API エンドポイントごとに関数を定義し、その型を一括で抽出するパターン。

const api = {
  getUser: (id: string) => fetch(`/users/${id}`).then(r => r.json()),
  createUser: (name: string, email: string) =>
    fetch("/users", {
      method: "POST",
      body: JSON.stringify({ name, email }),
    }).then(r => r.json()),
};

type API = typeof api;
type GetUserParams = Parameters<API["getUser"]>;  // [id: string]
type CreateUserParams = Parameters<API["createUser"]>;  // [name: string, email: string]

Awaited との組み合わせ

非同期関数の場合、ReturnTypePromise<T> を返す。中身の型を取り出すには Awaited を使う。

async function fetchData() {
  const res = await fetch("/api/data");
  return res.json() as Promise<{ items: string[] }>;
}

type RawReturn = ReturnType<typeof fetchData>;
// Promise<{ items: string[] }>

type Data = Awaited<ReturnType<typeof fetchData>>;
// { items: string[] }
Awaited

Promise<T> から T を取り出す。ネストした Promise も再帰的に解決する。TypeScript 4.5 で追加された。

よくあるパターン

Awaited<ReturnType<typeof 非同期関数>> は頻出する。API 呼び出しの結果型を取得するのに使う。

ConstructorParameters と InstanceType

クラス向けにも同様のユーティリティ型がある。

class User {
  constructor(public name: string, public age: number) {}
}

type UserConstructorParams = ConstructorParameters<typeof User>;
// [name: string, age: number]

type UserInstance = InstanceType<typeof User>;
// User

ConstructorParameters はコンストラクタの引数、InstanceType はインスタンスの型を取得する。

注意点

オーバーロードされた関数の場合、最後のシグネチャのみが抽出される。

function parse(input: string): string[];
function parse(input: number): number[];
function parse(input: string | number): string[] | number[] {
  // 実装
}

type ParseReturn = ReturnType<typeof parse>;
// string[] | number[](最後のシグネチャ)

複数のオーバーロードすべてを扱いたい場合は、別途対策が必要になる。

ReturnTypeParameters を使いこなすと、型定義の DRY(Don’t Repeat Yourself)を実現できる。既存の関数から型を導出する習慣をつけよう。