TypeScriptの特徴の1つに型推論があり、TypeScriptコンパイラは変数に代入された値に基づいてその型を推論することができます。
型推論を利用すると、多くの場合、明示的な型注釈を省略できるため、コードをより簡潔で読みやすくすることができます。特に、文脈から変数の型が明らかな場合に有効になります。
型推論の主な利点の1つは、実行時ではなく、コンパイル時にバグやエラーを発見しやすくなることです。たとえば、数値と推論された変数に文字列を代入しようとすると、TypeScriptコンパイラはエラーを投げ、その問題を警告してくれます。
特に大規模なプロジェクトでは、手作業による型注釈は面倒であり、エラーが発生しやすいため、これは多くの時間とフラストレーションを節約することができます。
let name = "John"; // 変数nameは文字列として推論されます
let age = 30; // 変数ageは数値として推論されます
let isProgrammer = true; // 変数isProgrammerはブール値として推論されます
型推論を行わない場合は、次のように型を明示的に指定する必要があります。
let name: string = "John";
let age: number = 30;
let isProgrammer: boolean = true;
関数を扱う際に特に有効で、関数の引数や戻り値の型を明示的に宣言することなく指定することができます。以下は同じ挙動を示します。
function add(x: number, y: number): number {
return x + y;
}
function add(x, y) {
return x + y;
}
この場合、xとyの型は明示的に宣言されていません。しかし、TypeScriptのコンパイラは、数値に対してのみ使用できる+演算子の使い方から、それらの型を推測することができます。その結果、コンパイラはxとyを数値として扱い、戻り値も数値として扱います。
Note: TypeScriptでは関数の引数と戻り値に型注釈を注釈を付けることができます。戻り値の型注釈はTypeScriptによって推論されるので省略することが可能ですが、引数に関しては特定の場合を除いて型は推論されないので基本的に型注釈を付けることになります。
型推論は、デフォルト値を持つ関数の引数にも使用できます。
function add(x: number, y = 0): number {
return x + y;
}
この場合、yのデフォルト値は0であり、これは数値であるため、yの引数の型は数値と推論されます。
また、TypeScriptは関数から返される値に基づいて型を推論することもできます。
function getName(): string {
return "John";
}
let name: string = getName(); // 変数nameは文字列として推論されます
上の例では、getName()関数が文字列を返すので、変数nameの型は文字列と推論されます。
型推論は、特定の型のプレースホルダーを指定することができる型であるジェネリクスでも使用することができます。ジェネリクスを使用すると、型推論システムがジェネリクスが使用されている文脈に基づいて実際の型を自動的に決定します。
function identity<T>(arg: T): T {
return arg;
}
let name = identity("John"); // 変数nameは文字列として推論されます
let age = identity(30); // 変数ageは数値として推論されます
上記の例では、identity()関数は型Tの引数を1つ取り、同じ型の値を返す汎用関数です。この関数が文字列引数で呼び出された場合、変数nameの型は文字列と推論されます。同様に、この関数が数値引数で呼ばれた場合、変数ageの型は数値と推論されます。
型推論は高階関数を扱う場合に特に有効になります。なぜなら、コンパイラは引数として渡され、結果として返される関数の型を正確に推論することができるからです。高階関数は複雑な型関係を持つことが多く、手作業で型付けするのが難しいです。
例えば、次のような高階関数を考えてみましょう。
function f<T, U>(array: T[], callback: (item: T) => U): U[] {
return array.map(callback);
}
let numbers = [1, 2, 3, 4 ,5];
let squaredNumbers = f(numbers, n => n * n);
let strings = f(numbers, n => n.toString());
console.log(squaredNumbers); // [1, 4, 9, 16, 25]
console.log(strings); // ['1', '2', '3', '4', '5']
この例では、map関数はT型の配列を取り、U型の結果を返すコールバック関数を受け取ります。次にmap関数は配列の各要素にコールバック関数を適用し、変換後の値(U型)の新しい配列を返します。
型推論を行わない場合、map関数を呼び出すたびにTとUの型を手動で設定する必要があります。特に、複雑な型や大量の関数呼び出しを扱う場合、これは面倒で間違いを起こしやすいでしょう。
型推論は、TとUが使われる文脈からコンパイラが自動的に型を推論することで、この負担を軽減することができます。これにより、正確かつ効率的なコードを書くことが容易になり、型エラーを防ぐことができます。
Note: 高階関数やコールバックについては、CS基礎/上級/ラムダ関数で詳しく学習できます。
TypeScriptで型推論を行う場合、nullとundefinedの扱いに注意が必要です。
TypeScriptのデフォルトでは、strictNullChecksコンパイラオプションがtrueに設定されており、nullとundefinedは他の型に代入できない別の型として扱われます。これは、nullやundefinedの値が誤って使用されることによるエラーを防ぐのに役立ちます。
let x = 3;
x = null; // xはnumber型であると推測され、nullはnumberに代入できないためエラー
しかし、これは特に型推論を使う場合に、nullや未定義になりうる変数の型を明示的に指定する必要があることも意味します。
let y: number | null = 3;
y = null; // yは数値またはnull型なのでokです
let z: number | undefined;
z = undefined; // zは数値またはundefined型なのでOKです
数値とnull値が混在する配列がある場合、その配列の型は(number | null)[]と推論されます。これは、その配列が数値かnullのどちらかの要素を持つ配列であることを意味します。以下は同じ挙動を示します。
let numbers = [1, 2, null, 3];
let numbersWithTypeAnnotation: (number | null)[] = [1, 2, null, 3];
この例では、配列にnumberとnullの両方の値が含まれているため、numberの型は(number | null)[]と推論されます。
配列の要素の型はユニオン型であり、numberかnullのどちらかになることを意味します。