まず「アロー関数の型推論」で何が起きているか
アロー関数はこういう形の関数です。
const add = (a, b) => a + b;
TypeScriptTypeScript では、ここに 全部の型を書かなくても、
コンパイラが「文脈」から型を推論してくれます。
アロー関数の型推論で大事なのは、この2つです。
- 引数の型をどうやって決めているか
- 戻り値の型をどうやって決めているか
そしてもう1つ、
「どこまで推論に任せてよくて、どこからは明示した方がいいか」
という感覚です。
ここから、例を通してじっくり固めていきます。
基本1:戻り値の型は「書いた式」から自動で決まる
単純なアロー関数の推論
const add = (a: number, b: number) => {
return a + b;
};
TypeScriptこのとき、add の型は自動的にこう推論されます。
// 推論される型
const add: (a: number, b: number) => number;
TypeScript戻り値の型 number は、return a + b の式から決まっています。
1行で書いても同じです。
const add = (a: number, b: number) => a + b;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ここから (a: number, b: number) => number と推論される
TypeScriptここでのポイントは、
アロー関数は「本体の式」から戻り値の型をほぼ自動で決めてくれるので、
戻り値の型をわざわざ書かなくていい場面が多い
ということです。
戻り値の型を明示したいときだけ、こう書きます。
const add = (a: number, b: number): number => a + b;
TypeScriptでも、基本的には省略しても問題ありません。
基本2:引数の型は「文脈」から推論される
配列メソッドとの組み合わせでよく起きる推論
一番分かりやすいのが map などの配列メソッドです。
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);
TypeScriptここで、(n) => n * 2 の n に型を書いていません。
それでも TypeScript は n を number と推論します。
なぜかというと、
numbersはnumber[]Array.prototype.mapの型は(callback: (value: number, index: number, array: number[]) => U) => U[]- だから、
callbackの第1引数valueはnumber
という情報があるからです。
つまり、
「アロー関数が“どこに渡されているか”を見て、その引数の型を決めている」
これが「文脈からの型推論」です。
自分で関数の引数に「関数型」を書いた場合も同じ
type Mapper = (value: string) => number;
function useMapper(fn: Mapper) {
const result = fn("hello");
console.log(result);
}
useMapper((v) => v.length);
TypeScriptここでも (v) => v.length の v に型を書いていませんが、Mapper が (value: string) => number なので、v は string と推論されます。
「アロー関数の引数の型は、“受け取る側の関数の型”から逆算される」
というイメージを持っておくと、かなりスッキリします。
基本3:型推論が効かないときは「noImplicitAny」で気づける
文脈がないと引数は any になりがち
例えば、こう書いたとします。
const fn = (x) => {
return x * 2;
};
TypeScriptこの x は、文脈がなければ any と推論されます。
(noImplicitAny が有効ならエラーになります)
fn の型もこうなります。
const fn: (x: any) => any;
TypeScriptこれは TypeScript 的にはあまり嬉しくない状態です。
「型安全にしたいのに、結局 any になっている」からです。
こういうときは「引数の型だけは明示する」が基本
文脈がないアロー関数では、
「引数の型だけはちゃんと書く」 をルールにしておくと安全です。
const fn = (x: number) => {
return x * 2;
};
TypeScript戻り値の型は number と推論されるので、
わざわざ書かなくても OK です。
「引数の型は自分で決める」「戻り値は式から推論させる」
この分担が、アロー関数と型推論の一番気持ちいい使い方です。
応用1:アロー関数とジェネリクスの型推論
map 的な「汎用関数」を自分で書く場合
例えば、配列を変換する関数を自作するとします。
function mapArray<T, U>(array: T[], fn: (value: T) => U): U[] {
const result: U[] = [];
for (const item of array) {
result.push(fn(item));
}
return result;
}
TypeScriptこれを使うとき、アロー関数の型推論が効きます。
const lengths = mapArray(["a", "bb", "ccc"], (s) => s.length);
TypeScriptここで起きている推論はこうです。
arrayにstring[]を渡したので、Tはstringfnの型は(value: T) => Uなので、valueはstring(s) => s.lengthのsはstringと推論されるs.lengthはnumberなので、Uはnumber- 結果として
mapArrayの戻り値はnumber[]
つまり、ジェネリック関数の型パラメータ(T, U)も、アロー関数の中身から逆算されて決まる わけです。
ここまで来ると、
「アロー関数の型推論」
=「呼び出し側の引数・返り値・アロー関数の中身・ジェネリクスが全部つながって、TypeScript が一気に解いてくれている」
というイメージが持てるはずです。
応用2:オブジェクトのメソッドにアロー関数を使うときの推論
オブジェクトリテラル+アロー関数
const user = {
name: "Taro",
greet: () => {
console.log("こんにちは");
},
};
TypeScriptここでは greet の型は () => void と推論されます。
ただし、アロー関数は this を持たないので、this を使いたいメソッドには向きません。
const user = {
name: "Taro",
greet() {
console.log("こんにちは、" + this.name); // こっちはOK
},
};
TypeScriptアロー関数は「this を外側から捕まえる」性質があるので、
クラスのプロパティやコールバックには向いていますが、
オブジェクトリテラルのメソッドで this を使いたいときは、
通常のメソッド記法の方が TypeScript 的にも素直です。
ここでのポイントは、
アロー関数の型推論は便利だけど、「this をどう扱うか」まで含めて設計する必要がある
ということです。
どこまで推論に任せて、どこから明示するか
推論に任せていいところ
戻り値の型(単純な式ならほぼ任せてOK)
配列メソッド(map, filter など)のコールバック引数
ジェネリック関数に渡すアロー関数の引数・戻り値
これらは、文脈がしっかりしているので、
TypeScript がかなり賢く推論してくれます。
明示した方がいいところ
文脈のないアロー関数の引数((x) => ... だけ書いている場合)
外部に公開する「ライブラリ的な関数」の型(API の顔になる部分)
複雑なジェネリクスで、推論結果を自分でコントロールしたいとき
特に初心者のうちは、
「引数の型は自分で書く」「戻り値は推論に任せる」
を基本ルールにしておくと、
「どこが any になっているか分からない」という事故をかなり防げます。
まとめ:「アロー関数の型推論」を自分の言葉で言うと
最後に、あなた自身の言葉でこう整理してみてください。
アロー関数は、
「戻り値の型は中で書いた式から勝手に決めてくれる」
「引数の型は“どこに渡しているか”という文脈から逆算してくれる」
だから、
配列の map や、自分で定義した (value: T) => U みたいな関数型と組み合わせると、
ほとんど型を書かなくても、ちゃんと型安全なコードになる。
ただし、文脈がないアロー関数は、
引数が any になりやすいので、
「引数の型だけはちゃんと書く」を自分ルールにしておくと安心。
コードを書きながら、
「このアロー関数の引数の型、TypeScript はどこから推論しているんだろう?」
と一度立ち止まって考えてみてください。
その問いを繰り返すうちに、
型推論を「なんとなくの魔法」ではなく、
自分でコントロールできる道具として扱えるようになっていきます。
