Menu

TypeScriptの関数型: アロー構文とコールバックの型

TypeScriptで関数の型を書く方法。アロー構文 (a: number) => string、関数の型エイリアス、アロー関数とコールバックの型付け、呼び出しシグネチャとコンストラクトシグネチャ、そして Function 型がゆるすぎる理由を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

関数型は関数の引数と戻り値を表す型で、アローを使って (a: number, b: number) => number のように書きます。type で名前を付ければ、変数、引数、オブジェクトのプロパティに使えます。

add に代入した関数には型注釈が要りません。引数の型は Operation から決まります。これを文脈による型付け(contextual typing)といい、関数型に名前を付ける一番の理由です。

関数型の構文

形は (parameters) => ReturnType です。各引数には名前と型が必要です。名前は読みやすさとエディターのヒントのためのもので、引数の型が互換であれば、引数名が何であっても一致します。

type Predicate = (value: number) => boolean;
type Formatter = (value: number, digits?: number) => string; // optional parameter
type Logger = (...parts: string[]) => void;                    // rest parameter
type Factory = () => { id: number };                           // no parameters

名前は省略できません。(string) => void と書くと、型のない string という名前の引数を宣言したことになり、strict モードでは TS7051: Parameter has a name but no type. Did you mean 'arg0: string'? で拒否されます。(value: string) => void と書きましょう。

アロー関数に型を付ける

アロー関数の型付けには2通りあります。関数そのものに注釈を書くか、変数に関数型を付けて引数の型を推論させるかです。

独立した関数なら1の形が一般的です。3の形は、複数の関数が同じシグネチャを共有するとき(ハンドラー、比較関数、変換関数)に効果を発揮します。シグネチャを1回書けば済み、変更したときはすべての箇所がチェックされます。

コールバックの型

関数型を型に持つ引数がコールバックです。呼び出し側が渡す関数はその型と照らし合わせてチェックされ、関数の引数の型もそこから推論されます。

コールバックを使いやすくしているルールが2つあります:

  • 引数が少ないのは問題ありません。 引数を1つだけ取る関数を、2つ渡される場所に渡せます。JavaScriptは余分な引数を無視するので、TypeScriptも許可します。型が渡すよりも多くの引数を取る関数はエラーです(Target signature provides too few arguments.)。
  • void の戻り値は何でも受け付けます。 (...) => void と型付けされたコールバックは値を返してもかまいません。呼び出し側はそれを使わないと約束しています。push は数値を返すのに list.forEach((x) => other.push(x)) がコンパイルできるのはこのためです。

ただし引数の型は互換である必要があります。(a: number, b: number) => number が期待される場所に (x: string) => ... を渡すと、Types of parameters 'x' and 'a' are incompatible. で失敗します。

呼び出しシグネチャ

(n: number) => string は、呼び出しシグネチャを持つオブジェクト型 { (n: number): string } の省略形です。関数がプロパティも持つ場合は長い形が必要です。

TypeScriptでは、同じスコープで const で宣言した関数にプロパティを追加でき、そのプロパティは関数の型に反映されます。インターフェースにも呼び出しシグネチャを書けます: interface Counter { (): number; count: number }。

コンストラクトシグネチャは new で呼び出すものを表します。前に new を付けて、new (name: string) => User、オブジェクトの形なら { new (name: string): User } と書きます。クラスを受け取る引数にはこの方法で型を付けます。

メソッド構文とプロパティ構文

オブジェクト型の中では、関数のメンバーをメソッドとして handle(value: string): void と書くことも、関数を持つプロパティとして handle: (value: string) => void と書くこともできます。同じに見えますが、strict のもとではチェックのされ方が違います:

コンパイラーは次のエラーを報告します:

index.ts(8,27): error TS2322: Type '(value: string) => void' is not assignable to type '(value: string | number) => void'.
  Types of parameters 'value' and 'value' are incompatible.
    Type 'string | number' is not assignable to type 'string'.
      Type 'number' is not assignable to type 'string'.

このエラーは正しい指摘です。onlyStrings は数値を扱えません。メソッドの形では同じ間違いが通ってしまい(歴史的な理由でメソッドの引数は「双変」でチェックされます)、a.handle(42) は実行時に TypeError: value.toUpperCase is not a function でクラッシュします。b の行を消すとそのクラッシュを確認できます。自分でオブジェクト型を書くなら、プロパティの形のほうが多くの間違いを捕まえられます。

Function 型は避ける

Function は、あらゆる関数値が満たす組み込みの型です。これが望ましいことはほとんどありません。TypeScriptは引数も戻り値の型もわからないので、どんな呼び出しも受け付け、結果は any になります。

Function は本当のシグネチャに置き換えましょう。本当にどんな関数でも受け付けたい場合(汎用の debounce やログ用のラッパー)、(...args: never[]) => unknown ならあらゆる関数を受け付けつつ、結果を any ではなく unknown に保てます。入力によって型が決まる関数については ジェネリクス を参照してください。ジェネリックな関数型は <T>(value: T) => T のようになります。

よくある質問

TypeScriptで関数型を定義するには?

アロー構文を使います: (a: number, b: number) => number。型エイリアスで名前を付け(type Compare = (a: number, b: number) => number;)、その名前を変数、引数、プロパティに使います。引数名は構文の一部ですが、ドキュメントのためだけのものです。引数名が違う関数でも型は一致します。

TypeScriptでコールバック引数に型を付けるには?

関数型を引数の型として書きます: function onEach(items: string[], cb: (item: string, index: number) => void) { ... }。呼び出し側は引数の少ない関数を渡すこともでき、コールバック自身の引数の型はその型から推論されるので、onEach(list, (item) => ...) には型注釈が要りません。

TypeScriptで Function 型を使うべきでないのはなぜですか?

Function はどんな関数でも受け付け、呼び出しがチェックされません。どんな引数でも渡せて、結果は any になります。代わりに () => void や (value: string) => number のように本当のシグネチャを書きましょう。本当にあらゆる関数を受け付けたいなら (...args: never[]) => unknown を使います。

関数型と呼び出しシグネチャの違いは何ですか?

同じものを表しています。(n: number) => string はオブジェクト型 { (n: number): string } の省略形です。関数がプロパティも持つ場合は、{ (n: number): string; label: string } のように呼び出しシグネチャの形が必要です。

TypeScriptでアロー関数に型を付けるには?

引数と戻り値の型をその場で書く方法(const half = (n: number): number => n / 2;)と、変数に関数型を付けて引数の型を推論させる方法(const half: (n: number) => number = (n) => n / 2;)のどちらかです。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める