1. ホーム
  2. TypeScript

【TypeScript】ReturnType・Parameters の使い方|関数の戻り値・引数の型を取り出す

Share

TypeScript でコードを書いていると、「ある関数が返す型を、それを使う側でもう一度書く」場面によく出くわします。関数の戻り値の形を変えるたびに、受け取る変数の型も手で直すのは面倒ですし、直し忘れれば型のズレが起きます。こうした「型の二重管理」を避けるために用意されているのが ReturnType<T>Parameters<T> というユーティリティ型です。この記事では、既存の関数から戻り値の型・引数の型を取り出して再利用する方法を、typeof との組み合わせや async 関数での注意点まで、初心者〜中級者向けに解説します。

なぜ「型を取り出す」必要があるのか

たとえばユーザー情報を組み立てて返す関数があり、その戻り値を別の場所で受け取りたいとします。素直に書くと、関数側と受け取る側の両方で同じ型を定義することになります。

duplicate-type.ts
function getUser() {
  return {
    id: 1,
    name: "田中",
    isAdmin: false,
  };
}

// 戻り値と同じ形を、受け取る側でもう一度手書きしている
let currentUser: { id: number; name: string; isAdmin: boolean };
currentUser = getUser();

この書き方だと、getUser の返す形に email を足したときに、currentUser の型も手で直さなければなりません。直し忘れると型がズレて、せっかくの型チェックが役に立たなくなります。ReturnTypeParameters を使えば「関数が返す型」「関数が受け取る型」を関数側から自動で取り出せるので、こうした重複を1か所にまとめられます。

ReturnType<T> で戻り値の型を取り出す

ReturnType<T> は、関数型 T の戻り値の型を取り出すユーティリティ型です。ここで大切なのは、T に渡すのは「関数そのもの(値)」ではなく「関数の型」だという点です。関数名から型を得るには typeof を組み合わせて ReturnType<typeof getUser> と書きます。

return-type.ts
function getUser() {
  return {
    id: 1,
    name: "田中",
    isAdmin: false,
  };
}

// getUser の戻り値の型を取り出して名前を付ける
type User = ReturnType<typeof getUser>;
// User は { id: number; name: string; isAdmin: boolean } になる

let currentUser: User;
currentUser = getUser(); // OK

// getUser 側に email を足せば、User にも自動で反映される

type User = ReturnType<typeof getUser> と書くと、getUser が返すオブジェクトの型がそのまま User になります。あとから getUser の戻り値に項目を足しても、User は自動で追従するので、受け取る側の型を手直しする必要がありません。これが型の二重管理を避けるということです。

Parameters<T> で引数の型を取り出す

Parameters<T> は、関数の引数の型を「タプル型」として取り出します。タプルとは、要素の型と順番が決まった配列のことです。引数が2つある関数なら、2要素のタプルとして取り出されます。個別の引数の型が欲しいときは、タプルの要素にインデックスでアクセスして Parameters<typeof fn>[0] のように書きます。

parameters.ts
function createUser(name: string, age: number, isAdmin: boolean) {
  return { name, age, isAdmin };
}

// 引数全体をタプルとして取り出す
type CreateUserArgs = Parameters<typeof createUser>;
// CreateUserArgs は [name: string, age: number, isAdmin: boolean]

// 個別の引数の型は、インデックスで取り出す
type NameArg = Parameters<typeof createUser>[0]; // string
type AgeArg = Parameters<typeof createUser>[1];  // number

// タプルを使えば、引数をまとめて受け渡しできる
const args: CreateUserArgs = ["田中", 30, false];
const user = createUser(...args);

Parameters<typeof createUser> は3つの引数の型を並べたタプルになります。全体をそのまま使えば、引数をまとめて配列で持ち回してスプレッド構文で渡す、といったことができます。特定の引数の型だけが欲しい場合は [0][1] のようにインデックスで取り出せば、その位置の引数の型が得られます。

ここまでで登場した2つのユーティリティ型を、いったん整理しておきます。

ユーティリティ型取り出すもの
ReturnType<T>関数型 T の戻り値の型
Parameters<T>関数型 T の引数の型(タプル)

実践例:API を返す関数の型を使い回す

実際の開発では、API からデータを取得する関数の戻り値を、それを表示するコンポーネントや state で再利用したい場面が多くあります。関数側で型が決まっていれば、ReturnType でその型を引き出して使い回せます。

reuse-return-type.ts
// 商品データを組み立てて返す関数
function buildProduct(id: number, name: string, price: number) {
  return {
    id,
    name,
    price,
    tax: Math.floor(price * 0.1),
  };
}

// 戻り値の型を取り出して、他の場所で使い回す
type Product = ReturnType<typeof buildProduct>;

// 一覧を保持する変数の型として使う
const cart: Product[] = [];

// 商品1件を受け取って表示する関数の引数型としても使える
function renderProduct(product: Product): string {
  return `${product.name}:${product.price + product.tax}円`;
}

buildProduct の戻り値に tax のような計算結果が含まれていても、ReturnType で取り出した Product にはそれが正しく反映されます。あとから返す項目が増えても、Product を使っている箇所はすべて自動で追従します。関数を「型の定義元」として扱えるのが、このパターンの便利なところです。

イベントハンドラの引数型を取り出す

Parameters は、あるハンドラ関数と同じ引数を受け取る別の関数を書きたいときにも役立ちます。ハンドラの引数の型を Parameters<typeof handler>[0] で取り出せば、引数の型を手で書き写す必要がなくなります。

handler-parameter.ts
function onSubmit(event: { name: string; value: number }) {
  console.log(event.name, event.value);
}

// onSubmit の第1引数の型を取り出す
type SubmitEvent = Parameters<typeof onSubmit>[0];
// SubmitEvent は { name: string; value: number }

// 同じ引数型を使うラッパー関数を書ける
function logAndSubmit(event: SubmitEvent): void {
  console.log("送信します");
  onSubmit(event);
}

onSubmit の引数の型が変われば SubmitEvent も自動で変わるので、ラッパー側の型を直す必要がありません。関数のシグネチャを1か所で管理し、それを参照する形にできます。

コンストラクタやインスタンスの型を取り出す関連ユーティリティ

関数だけでなく、クラスに対しても似たユーティリティ型が用意されています。主役は ReturnTypeParameters ですが、クラスを扱うときに合わせて覚えておくと便利なのが次の2つです。ConstructorParameters<T> はコンストラクタの引数の型をタプルで、InstanceType<T> はクラスから生成されるインスタンスの型を取り出します。

class-utilities.ts
class ApiClient {
  constructor(public baseUrl: string, public timeout: number) {}

  get(path: string) {
    return fetch(this.baseUrl + path);
  }
}

// コンストラクタの引数の型(タプル)
type ClientArgs = ConstructorParameters<typeof ApiClient>;
// ClientArgs は [baseUrl: string, timeout: number]

// new したときに得られるインスタンスの型
type Client = InstanceType<typeof ApiClient>;
// Client は ApiClient と同じ型

const args: ClientArgs = ["https://example.com", 5000];
const client: Client = new ApiClient(...args);

クラスに対して typeof ApiClient と書くと「コンストラクタの型」が得られ、そこから ConstructorParameters で引数、InstanceType でインスタンスの型を取り出せます。関数の ReturnType / Parameters と発想は同じで、「既存の定義から型を導出する」ための道具立てだと考えると理解しやすいです。

typeof の付け忘れと async 関数のつまずき

ReturnTypeParameters を使い始めたときにつまずきやすいポイントが2つあります。どちらもエラーメッセージだけ見ると原因が分かりにくいので、仕組みを押さえておきましょう。

ReturnType<関数> ではなく ReturnType<typeof 関数> と書く

よくある間違いが、typeof を付けずに ReturnType<getUser> と書いてしまうことです。これはエラーになります。理由は、TypeScript では「値の名前空間」と「型の名前空間」が別々に存在するからです。getUser という名前は値(関数の実体)を指しており、型ではありません。typeof を付けることで、その値から型を取り出してユーティリティ型に渡せます。

typeof-required.ts
function getUser() {
  return { id: 1, name: "田中" };
}

// NG: getUser は「値」なので型としては使えない
// 'getUser' refers to a value, but is being used as a type here.
type Wrong = ReturnType<getUser>;

// OK: typeof で値から型を取り出してから渡す
type Right = ReturnType<typeof getUser>;

エラーメッセージに「refers to a value, but is being used as a type here」と出たら、typeof の付け忘れを疑ってください。関数名・変数名など「値」を型の文脈で使うときは、原則 typeof が必要になります。

async 関数の戻り値は Awaited で中身を取り出す

もう1つの注意点は async 関数です。async 関数の戻り値は必ず Promise でくるまれるため、ReturnType で取り出せるのも Promise<...> という型になります。実際に欲しいのは await したあとの中身の型であることが多いので、その場合は Awaited<T> を組み合わせて Awaited<ReturnType<typeof fn>> と書きます。

async-return-type.ts
async function fetchUser() {
  return { id: 1, name: "田中" };
}

// ReturnType だけだと Promise でくるまれた型になる
type Raw = ReturnType<typeof fetchUser>;
// Raw は Promise<{ id: number; name: string }>

// Awaited で中身を取り出す
type User = Awaited<ReturnType<typeof fetchUser>>;
// User は { id: number; name: string }

async function main() {
  const user: User = await fetchUser(); // OK
  console.log(user.name);
}

Awaited<T>Promise を1枚(必要なら何枚でも)はがして、最終的に解決される値の型を返すユーティリティ型です。async 関数から得た型が思ったより Promise<...> になっている、というときは Awaited で包んであげると期待どおりの型になります。

まとめ

ReturnType<T> は関数型の戻り値の型を、Parameters<T> は引数の型をタプルとして取り出すユーティリティ型です。関数名から型を得るときは typeof を組み合わせて ReturnType<typeof getUser> のように書き、個別の引数は Parameters<typeof fn>[0] でインデックスアクセスします。これらを使えば、関数を「型の定義元」として扱い、戻り値や引数の型を1か所にまとめて二重管理を防げます。クラスを扱うときは ConstructorParameters<T>InstanceType<T> が同じ発想で使えます。つまずきやすいのは typeof の付け忘れと、async 関数の戻り値が Promise でくるまれる点です。後者は Awaited<ReturnType<typeof fn>> で中身を取り出せることを覚えておけば、実務でそのまま使えます。

参考ページ