JavaScriptにおいて関数はオブジェクトであるので、以下のように他のオブジェクトと同様の扱いをすることができます。
TypeScriptではその関数を型システムを利用して表現していきます。初めに簡単な関数の例を見てみましょう。
function add(x: number, y: number): number {
return x + y;
}
let result = add(10, 20);
console.log(result); // 30
この例では、add関数はxとyという2つのパラメータを受け取りますが、どちらもnumber型です。この関数の戻り値の型はnumberで、これはnumber型の値を返すことを意味します。
TypeScriptでは関数の引数と戻り値に型注釈を注釈を付けることができます。戻り値の型注釈はTypeScriptによって推論されるので省略することが可能ですが、引数に関しては特定の場合を除いて型は推論されないので、基本的に型注釈を付けることになります。
上記では関数を宣言するためにfunction構文を使用しましたが、他にも関数を宣言する方法はあるのでいくつか見ていきましょう。
TypeScriptでは、関数の型を定義し、変数に代入することができます。関数型は、その関数の引数の型と戻り値の型から構成されます。
let add = function(x: number, y: number): number {
return x + y;
};
let result = add(10, 20);
console.log(result); // 30
この例では、変数addがnumber型の2つの引数を取り込み、number型の値を返す関数であることを意味しています。
この構文は、function構文をより短くしたものです。この構文では、関数の引数と戻り値の型を本体から分離するために=>演算子を使用します。
let add = (x: number, y: number): number => {
return x + y;
};
let result = add(10, 20);
console.log(result); // 30
以下のように、さらに短縮することもできます。
let add = (x: number, y: number): number => x + y;
let result = add(10, 20);
console.log(result); // 30
TypeScriptでは、引数の最後に?を付けることで、引数をオプションとしてマークすることができます。
function greet(name: string, age?: number): string {
let greeting = `Hello, ${name}`;
if (age) {
greeting += `, your age is ${age}`;
}
return greeting;
}
console.log(greet("John")); // "Hello, John"
console.log(greet("John", 30)); // "Hello, John, your age is 30"
この例では、引数ageはオプションとしてマークされています。これは、関数greetを呼び出すときに、引数ageに値を指定する必要がないことを意味します。もし値が渡さなければ、それは未定義となります。
TypeScriptでは、引数を宣言するときに値を代入することで、引数にデフォルト値を指定することができます。このデフォルト値は、関数が呼び出されたときに引数が未定義であった場合に使用されます。
function greet(name: string, age: number = 30): string {
return `Hello, ${name}, your age is ${age}`;
}
console.log(greet("John")); // "Hello, John, your age is 30"
console.log(greet("John", 40)); // "Hello, John, your age is 40"
この例では、引数ageのデフォルト値は30です。つまり、引数ageに値を指定せずに関数greetを呼び出すと、デフォルトの30が使われることになります。
TypeScriptでは、関数定義の中で残りの引数を表すために、スプレッド構文(...)を使用することができます。残余引数は、関数に渡される任意の数の末尾の引数を把握するために使用されます。これらの引数は、配列に格納されます。
以下は、残余引数を使用して、任意の数の追加引数を取得する関数の例です。
function printAll(first: string, second: string, ...others: string[]) {
console.log(first);
console.log(second);
console.log(others);
}
printAll("hello", "world", "this", "is", "a", "test");
// "hello", "world", ["this", "is", "a", "test"]
この例では、関数printAllは、2つの必須引数である第1引数と第2引数、および任意の数の追加引数を取ります。これらの追加引数は残りのパラメータothersで捕捉され、string[]型です。
もし関数に渡される末尾の引数が文字列型でない場合、TypeScriptはコンパイルエラーをスローします。末尾の引数はいくつでも許容されるため、可変数の引数が渡される場合に有用です。
function concatenate(separator: string, ...strings: string[]) {
return strings.join(separator);
}
console.log(concatenate(',', 'hi', 'how', 'are', 'you')); // hi,how,are,you
関数は他のデータ型と同様に、変数に格納したり、引数として渡したり、値として返したりすることができます。このため、1つ以上の関数を引数として受け取り、その結果を関数として返す高階関数を作成することができます。これは、map, reduce, filterなどの関数型プログラミングを実装するために使用できます。
// 関数を引数にとり、新しい関数を返す高階関数
function multiplyBy(factor: number) {
return function(num: number) {
return num * factor;
}
}
// multiplyBy関数を使って、3倍する新しい関数を作成
let triple = multiplyBy(3);
console.log(triple(5)); // 15
console.log(triple(10)); // 30
このコードでは、multiplyByという高階関数を定義しています。この関数は、number型のnumを引数として受け取り、numにfactorを掛けた結果を返す無名関数を返します。
最後にmultiplyBy(3)が返す関数を代入した変数tripleを作成し、5と10をそれぞれ引数としてその関数を呼び出しているので、num*3と同じ意味になります。この返された関数は、変数factorをそのスコープから取り込んでいるため、クロージャとも呼ばれます。
ここで重要なのは、multiplyBy関数は呼び出されるたびに新しい関数を返すということです。つまり、multiplyByが返す関数は、すべて同じ関数で作られたものであっても、それぞれ独立した状態を持つことができます。
それではmap関数の例を見てみましょう。
let numbers = [1, 2, 3, 4, 5];
let doubledNumbers = numbers.map(function(num) {
return num * 2;
});
console.log(doubledNumbers); // [2, 4, 6, 8, 10]
アロー関数を使って、省略して記述することもできます。
let numbers = [1, 2, 3, 4, 5];
let tripledNumbers = numbers.map(num => num * 3);
console.log(tripledNumbers); // [3, 6, 9, 12, 15]
関数のオーバーロードは、同じ名前で、異なる引数の型や数を持つ複数の関数シグネチャを定義することで実現されます。そして、実際の関数の実装は、最後のオーバーロードに配置されます。ある関数が引数のセットで呼び出されると、TypeScriptはオーバーロードを調べ、引数の型に基づいて最適なものを見つけようとします。
// シグネチャ部分
function add(a: number, b: number): number;
function add(numbers: number[]): number;
// 実装部分
function add(number: number | number[], b?: number) {
if (typeof number === "number") {
return number + b;
}
return number.reduce((accumulator, current) => {
return accumulator + current;
}, 0);
}
console.log(add(2, 3)); // 5
console.log(add([1, 2, 3, 4])); // 10
最初のシグネチャは2つの数値を引数として受け取り、数値を返します。2番目のシグネチャは、数値の配列を受け取り、同じく数値を返します。
関数のオーバーロードはTypeScriptの強力な機能であり、より表現力豊かで保守性の高いコードを書くことができるようになります。また、異なるシナリオを一つの関数で処理する方法を提供し、読みやすく、理解しやすいコードを書くことができます。
オーバーロードはコンパイル時のみに限定されることに留意してください。実行時には、TypeScriptは関数の実装部分を1つだけ持つことになります。
型ガードはTypeScriptの強力な機能で、何らかの条件に基づいて変数の型を絞り込むことができます。これはコード中のさまざまなシナリオに対応するため、またコードをより読みやすく、保守しやすくするために有用です。
型ガード関数とは、変数を引数にとり、その変数が特定の型に一致するかどうかをブール値で返す関数です。例えば、次の関数は変数がstring型であるかどうかをチェックします。
function isString(value: any): value is string {
return typeof value === 'string';
}
JavaScriptは豊富な解析サポートが組み込まれていません。JavaScriptで作業する場合、変数やオブジェクトの型を実行時に判断することが難しい場合があります。
JavaScriptのオブジェクトを使う場合、instanceofやtypeofのような組み込みの実行時型チェックツールを活用できない場合があります。
そこで、ユーザー定義の型ガード関数が使用できます。
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
function isPerson(obj: any): obj is Person {
return obj.hasOwnProperty('name') && obj.hasOwnProperty('age');
}
let obj = { name: 'John', age: 25 };
if (isPerson(obj)) {
console.log(`${obj.name} is ${obj.age} years old`);
}
上の例では、isPerson関数は渡されたobjがnameとageというプロパティを持つかどうかをチェックします。もし持っていれば、objはPersonクラスのインスタンスです。
ユーザー定義の型ガード関数はオブジェクトの構造に依存してその型を決定しているため、オブジェクトの構造が変化すると、誤った値を返す可能性がある点に注意が必要です。