TypeScript 関数を使ってみる
作成日:2023年11月12日
関連ページ
- TypeScript 環境の構築
- TypeScript を使ってみる(基本型や型注釈)
- TypeScript クラス
- TypeScript 高度な型
- TypeScript モジュール
- TypeScript 非同期処理 Promise と Fetch
関数
JavaScript や TypeScirpt で関数を定義するにはいくつかの方法があります。
TypeScirpt では、関数を定義する際に型注釈を書きます。以下は TypeScirpt で関数を使う際に必要となる型注釈の書き方や関数の型、関数型の部分型関係、ジェネリクスなど基本的なことについての覚書です。
参考サイト
- TypeScript Documentation (Handbook) Everyday Types Functions
- TypeScript Documentation (Handbook) More on Functions
- 関数(サバイバルTypeScript)
JavaScript の関数に関しては以下のサイトが参考になります。
関数宣言
関数宣言(function declaration)は関数を定義(作成)する最も基本的な方法です。
TypeScript では関数宣言の引数と戻り値に型注釈を書くことができます。
以下が構文です。引数はカンマ区切りで複数指定できます。
function 関数名( 引数: 型 ): 戻り値の型 { 処理内容 }
以下は add という関数を宣言する例です。
引数 x と y を number 型として宣言し、戻り値の型も number 型として宣言しています。
この場合、引数に number 型以外の値を指定して呼び出すとコンパイルエラーになります。また、引数の数もあっていないとコンパイルエラーになります。
TypeScript では、関数を呼び出す際の引数は、個数もデータ型も引数の定義通りに渡す必要があります。
function add(x: number, y: number): number { return x + y; } const result = add(3, 8); console.log(result); // 11 const result2 = add('a', 'b'); // コンパイルエラー // Argument of type 'string' is not assignable to parameter of type 'number'. const result3 = add(1); // コンパイルエラー // An argument for 'y' was not provided.
引数の型注釈を省略(コンパイルエラー)
引数の型注釈を省略するとコンパイラーは any 型と解釈するため、コンパイラーオプションの strict(noImplicitAny)によりコンパイルエラーになります。
return 文も型チェックされる
戻り値に型注釈を書いた場合は、return 文も型チェックが行われます。
以下の場合、戻り値の型を number 型としているのに return 文で文字列を返しているのでコンパイルエラーになります。
function add(x: number, y: number): number {
return `${x + y}`; // コンパイルエラー
// error TS2322 Type 'string' is not assignable to type 'number'
}
戻り値の型注釈を省略
戻り値の型注釈を省略することができます。戻り値の型注釈を省略した場合、コンパイラーが戻り値の型をコードから型推論します。
但し、型推論による戻り値の型が必ずしも期待する型でない可能性があります。
戻り値の型注釈を書くことで、関数の定義の中で戻り値に対するチェックが行えるようになります。
以下は関数 add は number 型の戻り値を期待するので、明示的に戻り値の型注釈を書いています。
function add(x: number, y: number): number { const result = x + y; return result; } const sum = add(12, 34); console.log(sum -10); // 36
もし、return 文の記述を忘れた場合はコンパイラーは関数が正しい戻り値を返していないので、関数の型注釈の部分にコンパイルエラーを表示します。
function add(x: number, y: number): number { // コンパイルエラー //A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value. const result = x + y; } const sum = add(12, 34); console.log(sum -10); // NaN
しかし、関数の戻り値の型注釈を省略すると、コンパイラーは戻り値を void 型と推論するため、関数部分にはコンパイルエラーは表示されず、関数の実行結果を使用する部分でコンパイルエラーが発生します。
function add(x: number, y: number) { const result = x + y; } const sum = add(12, 34); console.log(sum -10); // NaN(sum の部分にコンパイルエラーが表示される) // The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.
このように戻り値の型注釈を書くことで、関数の定義の中で戻り値に対するチェックが働くので、関数の実装が複雑な場合などでは実装ミスを見つけやすくなります。
return 文が複数ある場合
return 文が複数あり異なる型を返している場合は、推論される型はユニオン型になります。
以下の場合、推論される戻り値の型は number | null のユニオン型になります。
function divide(x: number, y: number) { if(y === 0) { return null; } return x / y; } console.log(divide(5,20)); // 0.25 console.log(divide(3,0)); // null
戻り値がない関数(void 型)
TypeScript で戻り値がない関数の戻り値を型注釈するには void 型という特殊な型を使用します。
以下は2つの number 型の引数を受け取って除算した結果をコンソールに出力するだけの戻り値のない関数の例です。戻り値の型に void 型を指定することによって戻り値がないことを宣言しています。
function printDivision(x: number, y: number):void { console.log(x / y); }
戻り値の型に void 型を指定しているのに、戻り値を返すとコンパイルエラーになります。
function printDivision(x: number, y: number):void {
return x / y; // number 型の値を返しているのでコンパイルエラー
// error TS2322: Type 'number' is not assignable to type 'void'.
}
戻り値の型に void 型を指定した関数では、通常 return 文がありませんが、早期リターン(戻り値のない return 文)を書くことはあります。※ 戻り値がある場合でも早期リターンは可能です。
function printDivision(x: number, y: number):void { if(y === 0) { // y が 0 であれば終了 return; // 早期リターン } console.log(x / y); }
関数式
関数式(function expression)を使って関数を定義することもできます。
以下が関数式の構文です。引数はカンマ区切りで複数指定できます。
関数宣言の構文と同じですが、この構文は式であり、式の評価結果が関数になります。言い換えると、式が評価されると関数を返します。
そのため、通常は定義した関数を使用するには変数に入れることになります(コールバック関数や即時関数として使用する場合など変数に入れないこともあります)。
また、関数宣言と同様に引数や戻り値の型注釈が書けます。
関数宣言同様、引数の型注釈を省略するとコンパイルエラーになり、return 文も型チェックされます。また、戻り値の型注釈は省略できます。
function 関数名( 引数: 型 ): 戻り値の型 { 処理内容 }
関数名の省略
定義した関数式は変数名で参照できるので関数名は省略可能です。
また、名前がない関数なので、匿名関数や無名関数とも呼ばれます。
const 変数名 = function ( 引数: 型 ): 戻り値の型 { 処理内容 };
関数式で定義した関数を呼び出すには変数名を使います。
const add = function (x: number, y: number): void { console.log(x + y); }; add(5, 8); // 13
関数式には巻き上げがない
関数宣言と関数式の違いの1つに巻き上げ(hoisting)の有無があります。
関数宣言には巻き上げがあり、以下は関数の定義の前に関数を呼び出していますがエラーになりません。
hello('foo'); function hello(name: string): void { console.log(name? 'hello ' + name: 'hello world'); }
関数式で関数を定義した場合は巻き上げがないため、以下はエラー(JavaScript の Uncaught ReferenceError と TypeScirpt のコンパイルエラー)になります。
hello('foo'); // エラー // Uncaught ReferenceError: Cannot access 'hello' before initialization // error TS2454: Variable 'hello' is used before being assigned. const hello = function (name: string): void { console.log(name? 'hello ' + name: 'hello world'); }
アロー関数
アロー関数(arrow function)は関数を定義する関数式の一つで、評価されると関数を返します。アロー関数を使うと、通常の関数式より短い構文で(簡潔に)記述することができます。
TypeScript のアロー関数では関数宣言や関数式同様、引数や戻り値に型注釈が書けます。
以下がアロー関数式の構文です(引数はカンマ区切りで複数指定できます)。
const 変数名 = ( 引数: 型 ): 戻り値の型 => { 処理内容 };
以下は2つの number 型の引数を受け取り、それらを加算した値を返す関数の定義の例です。
const add = (x: number, y: number): number => { return x + y; }; console.log(add(5, 8));
以下は number 型の width と height プロパティと string 型のオプショナルな color プロパティを持つ Rect オブジェクトを受け取って面積を算出して返す関数です。
type Rect = { width: number; height: number; color?: string; } const getArea = (rect: Rect): number => { return rect.width * rect.height; } const rect1: Rect = { width: 50, height:30, } const rectArea = getArea(rect1); console.log(rectArea); // 1500
関数の引数には分割代入が使えるので、上記は以下のように書き換えることができます。
const getArea = ({width, height}: Rect): number => { return width * height; }
アロー関数の省略形
関数が1つの文のみの場合 {}
と return
が省略可能です。
上記の例の場合、1つの文のみなので以下のように省略して記述することができます。
const getArea = ({width, height}: Rect): number => width * height;
但し、戻り値の式としてオブジェクトリテラルを使う場合は注意が必要です。
以下は、戻り値としてオブジェクト(RectArea)を返すように書き換えたものです。
type Rect = { width: number; height: number; color?: string; } // 戻り値のオブジェクトの型 type RectArea = { area: number; } const getArea = ({width, height}: Rect): RectArea => { return {area: width * height}; } const rect1: Rect = { width: 50, height:30, } const rectArea = getArea(rect1); console.log(rectArea); //{area: 1500}
上記のアロー関数の定義の{}
と return
を省略して以下のように記述するとコンパイルエラーになってしまいます。これは=>
の右側の{}
がアロー関数の処理内容を囲む{}
とみなされてしまうためです。
// 以下はコンパイルエラー const getArea = ({width, height}: Rect): RectArea => {area: width * height};
エラーにならないようにするには、以下のようにオブジェクトリテラルを()
で囲みます。
const getArea = ({width, height}: Rect): RectArea => ({area: width * height});
引数のカッコを省略した場合は型注釈を書けない
JavaScript のアロー関数は、引数が1つだけの場合、引数のカッコが省略できますが、TypeScript のアロー関数では引数のカッコを省略した場合は、引数と戻り値のどちらも型注釈を書くことができません。
また、関数宣言と同様、引数の型注釈を省略するとコンパイラーは any 型と解釈するため、コンパイラーオプションの strict(noImplicitAny)によりコンパイルエラーになります。
// 引数のカッコを省略すると、引数と戻り値のどちらも型注釈を書けない
const double = num => num * 2; // 引数の型注釈を省略するとコンパイルエラー
// error TS7006: Parameter 'num' implicitly has an 'any' type.
コンパイラーオプションの strict(noImplicitAny)が有効でも、引数の型が推論できる場合は、引数のカッコを省略できます。
例えば、以下のように他の関数の引数(コールバック)にアロー関数を直接書く場合、引数の型は推論されるのでカッコを省略できます。
// 引数 val は number と推論される const arr = [12, 7, 3, 24].filter( val => val < 10); //const arr = [12, 7, 3, 24].filter((val: number): boolean => val < 10);//型注釈あり console.log(arr); // [7, 3] // 引数 elem は string と推論される ["foo", "bar", "baz"].forEach( elem => console.log(elem)); // foo bar baz //["foo","bar","baz"].forEach((elem:string):void => console.log(elem));//型注釈あり type User = { id: number; name: string; }; const users: User[] = [ {id:1, name:'foo'}, {id:2, name:'bar'}, {id:3, name:'baz'} ]; // 引数 user は User 型 と推論される const names = users.map( user => user.name ); // const names = users.map((user:User):string => user.name); // 型注釈あり console.log(names); // (3) ['foo', 'bar', 'baz']
メソッド
メソッドはオブジェクトのプロパティとしての関数で機能的には関数と同じです。
従来からのメソッドの定義は、オブジェクトリテラルの中で メソッド名: 関数式
のように記述します。
メソッドを呼び出すには、オブジェクト.メソッド名()
とします。
関数宣言や関数式同様、引数の型注釈を省略するとコンパイルエラーになり、return 文も型チェックされます(戻り値の型注釈は省略できます)。
以下は function 文とアロー関数の関数式を使って定義したメソッドの例です。
const obj = { double: function(num: number):number { return num * 2; }, triple: (num: number):number => num * 3 }; console.log(obj.double(3)); // 6 console.log(obj.triple(3)); // 9
メソッドの短縮記法
メソッドの短縮記法を使うと、オブジェクトリテラルの中で メソッド名(引数):{処理内容}
のように記述できます。TypeScirpt では メソッド名(引数: 型): 戻り値の型 {処理内容}
のように記述できます。
以下は前述の例をメソッドの短縮記法で書き換えたものです。
const obj = { double(num: number):number { return num * 2; }, triple(num: number):number { return num * 3; } }; console.log(obj.double(3)); // 6 console.log(obj.triple(3)); // 9
残余引数(可変長引数の関数)
関数が任意の数の引数を受け取れるように残余引数(rest パラメータ)を使うことができます。
残余引数は、関数の最後の引数に ...
の接頭辞を付けることで残りの引数を配列として受け取ることができます。
引数には型注釈が必要ですが、残余引数の場合、型は必ず配列型(またはタプル型)になります。
以下は 残余引数を使って与えられた引数を全て合計して返す関数の例です。残余引数の値が number 型なので残余引数の型注釈は number[]
になります。
function sum(...params: number[]): number{ let result = 0; for (const num of params) { result += num; } return result; } console.log(sum(1,2,3,4)); // 10
残余引数は必ず最後の引数でなければなりません。また、残余引数を複数持たせることはできません。
以下は第1引数に通常の引数を、残りを残余引数とする関数の例です。
const sum = (print: boolean, ...params: number[]): number | void => { let result = 0; for (const num of params) { result += num; } if(print) { console.log(result); }else{ return result; } } sum(true, 1,2,3); // 6 と出力 console.log(sum(false, 1,2,3)); // 6
可変長引数の関数呼び出しにスプレッド構文を使う
スプレッド構文...
を使うと配列を記述した場所に展開することができます。
このため、スプレッド構文は可変長の引数(残余引数)の指定によく使われます。
以下は可変長引数の関数呼び出しでスプレッド構文を使う例です。引数に指定するスプレッド構文は、通常の引数と混ぜたり、複数使うこともできます。
const sum = (...params: number[]): number => { let result = 0; for (const num of params) { result += num; } return result; } const nums: number[] = [1,2,3,4]; // スプレッド構文で引数を指定 console.log(sum(...nums)); // 10 console.log(sum(...nums, 10, ...nums)); // 30
但し、固定長の引数を持つ関数の引数にスプレッド構文を指定するとコンパイルエラーになります(このような使い方はあまりしないと思いますが)。
以下の場合、関数 sumTree は3つの固定長の引数を受け取りますが、引数に指定している変数 nums は number[]
型のため型チェックでエラーになります。
これは number[]
型は要素数不明の number 型の配列を表すためです。以下の場合、nums の要素数は3つですが、型としては要素数が不明なのでエラーとなります。
const sumTree = (x: number, y: number, z: number): void => { console.log(x + y + z); } const nums: number[] = [1,2,3]; // または nums = [1,2,3];(自動推論で number[] 型) sumTree(...nums); // // error TS2556: A spread argument must either have a tuple type or be passed to a rest parameter.
エラーを回避するには、エラーメッセージに書かれているように nums の要素数が3つであることをタプル型を使って示します(または、as const
を使ってもエラーを回避できます)。
const sumTree = (x: number, y: number, z: number): void => { console.log(x + y + z); } // タプル型(要素数が固定された配列)とする const nums: [number, number, number] = [1,2,3]; // または const nums = [1,2,3] as const; sumTree(...nums); // 6
オプショナル引数
オプショナル引数は引数を省略可能にする TypeScript 固有の機能です。
オプショナル引数は引数名の後ろに?
を付けて引数名?: 型
という構文を使います。
以下の関数 add の第3引数 print はオプショナルな引数としているので、第3引数に真偽値を指定するか、または省略することができます。
const add = (x:number, y:number, print?:boolean): number | void => { const result = x + y; if(print) { // print が true であれば計算結果を出力 console.log(result); }else{ // print が false と判定されれば計算結果を返す return result; } } console.log(add(3, 5)); // 8 console.log(add(3, 5, false)); // 8 add(3, 5, true); // 8
オプショナル引数が省略された場合、その引数には undefined
が入ります。そのため、第3引数 print を省略すると条件分岐では false と判定されます。
オプショナル引数の型は undefined とのユニオン型
オプショナル引数の型は、その引数の型と undefined のユニオン型になります。
上記の例では、引数 print は boolean | undefined
型になります。
オプショナル引数はそのまま使うと undefined の可能性があるためコンパイルエラーになります。
例えば以下の場合、引数 name は undefined になる可能性があるため、そのまま toUpperCase() を使用するとコンパイルエラーになります。
const hello = (name?: string):void => { console.log("Hello " + name.toUpperCase); // コンパイルエラー } // error TS18048: 'name' is possibly 'undefined'.
エラーを回避するには、オプショナル引数 name が undefined でないことを確認して使用します。
const hello = (name?: string): void => { if(name) { console.log('Hello ', name.toUpperCase() ); }else{ console.log('Hello world'); } }; hello(); // Hello world hello('foo'); // Hello FOO // または以下のいずれでも同じ const hello = (name?: string): void => { console.log(`Hello ${name? name.toUpperCase() : 'world'}`); }; const hello = (name?: string): void => { if (typeof name === "undefined") { console.log("Hello world"); } else { console.log("Hello ", name.toUpperCase()); } };
また、引数にデフォルト値を設定することで、その引数をオプショナルとすることができます。
オプショナル引数の後に普通の引数は指定できない
オプショナル引数は普通の引数の後に書かなければいけません。以下のようにオプション引数より後ろに普通の引数を指定すると、コンパイルエラーになります。
const greeting = (when?: string, who: string) => { 中略 } // コンパイルエラー
// error TS1016: A required parameter cannot follow an optional parameter.
デフォルト引数
関数の引数は指定しなければ undefined になります。デフォルト引数を設定すると、対応する引数が渡されない場合や undefined が渡された場合に設定したデフォルト値が使用されます。
必要であれば、デフォルト引数は引数リストの途中に書くこともできます。
TypeScirpt では 引数名: 型 = デフォルト値
のように型注釈を付けます。
const hello = (name: string = 'World'): void => { console.log("Hello " + name.toUpperCase()); }; hello(); // Hello WORLD(引数 name を省略した場合はデフォルト値が使われる) hello('bar'); // Hello BAR
型推論が効く
デフォルト引数を設定すると引数の型推論が効くので、引数の型注釈を省略することもできます。
const hello = (name= 'World'): void => { console.log("Hello " + name.toUpperCase()); };
関数の型
TypeScript では関数を表す型があります。
以下は関数式(アロー関数)を使った関数を変数 add に代入しています。
const add = (x: number, y: number): number => { return x + y; };
VS Code などで変数名 add にマウスオーバーすると型推論による変数(代入されている関数)の型が表示されます。
表示された (x: number, y: number) => number
が関数の型になります。
関数の型宣言
以下が関数型の構文です。関数の型宣言は、type 文(型エイリアス)を使うことができます。
type 型の名前 = (引数名: 引数の型) => 戻り値の型;
引数が複数ある場合は引数名: 引数の型
をカンマ区切りで並べます。残余引数やオプショナル引数も関数型の中で使うことができます(使用例)。
以下は2つの number 型の引数を受け取り number 型を返す関数の型宣言です。
type Add = (x: number, y: number) => number;
定義した関数の型宣言は、アロー関数や関数式の型注釈に使えます。
type Add = (x: number, y: number) => number; // アロー関数の型注釈(引数と戻り値の型注釈は省略可能) const add: Add = (x: number, y: number): number => { return x + y; };
type Add = (x: number, y: number) => number; // 関数式の型注釈(引数と戻り値の型注釈は省略可能) const add: Add = function (num1: number, num2: number): number { return num1 + num2; };
但し、関数宣言の型注釈には使えません。
関数の型宣言の引数の名前
関数型の引数名は、型チェックにおいて「型が同じであれば、引数名が異なっていても関数型は同じ」とみなされます。
例えば、(x: number) => number
型と (num: number) => number
型は同じとみなされます。
関数の実装側の型注釈の省略
関数の型宣言を型注釈に使った場合、関数の実装側の引数と戻り値の型注釈は省略できます。
// 関数の型宣言 type Add = (x: number, y: number) => number; // 上記で宣言した型をアロー関数の型注釈に使用(引数と戻り値の型注釈は省略) const add: Add = (x,y) => { return x + y; }; // 関数の型宣言 type Sum = (...args: number[]) => number; // 上記で宣言した型を関数式の型注釈に使用(引数と戻り値の型注釈は省略) const sum: Sum = function(...nums) { let result = 0; for (const num of nums) { result += num; } return result; }
以下は型宣言で引数をオプショナルとして、実装側の引数ではデフォルト引数を指定する例です。
// 第3引数をオプショナル引数に設定 type Add = (x: number, y: number, print?: boolean) => number| void; // 実装側では第3引数にデフォルト引数を指定 const add: Add = (x,y, print=false) => { if(print) { console.log(x + y); }else{ return x + y; } }; console.log(add(1, 2)); add(3, 4, true);
type 文を使わない場合
変数の宣言に型注釈を書くように、type 文(型エイリアス)を使わずに記述することもできます。
以下は type 文を使った場合の記述例です。
type Add = (x: number, y: number) => number; const add: Add = (x: number, y: number): number => { return x + y; }; console.log(add(1,2)); // 3
以下は上記を書き換えて、変数 add に直接関数の型の形注釈を書く例です。
const add: (x: number, y: number) => number = (x: number, y: number): number => { return x + y; }; console.log(add(1,2)); // 3
関数の実装側の引数と戻り値の型注釈は省略できるので、以下のように省略して記述することもできます。
const add: (x: number, y: number) => number = (x, y) => { return x + y; }; console.log(add(1,2)); // 3
オブジェクト型を使った関数の型宣言
通常の関数の型宣言の構文とは別に、オブジェクト型の中で使用できる以下のメソッド構文でも関数の型を宣言できます。
type 型の名前 = { (引数名: 引数の型): 戻り値の型; };
オブジェクト型の中でメソッド構文を使うことで、そのオブジェクトが関数のように呼び出される、つまり関数であるという型定義を行うことができます。
type Add = { (x: number, y: number): number; } const add: Add = (x, y) => x + y; console.log(add(1,2)); // 3
通常の関数の型宣言の構文とメソッド構文は書き方が異なるだけで、以下の2つは同じ型になります。
// メソッド構文 type Add1 = { (x: number, y: number): number; } // 通常の関数の型宣言の構文 type Add2 = (x: number, y: number) => number;
メソッド構文を使った関数の型宣言では interface を使うこともできます。
interface Add { (x: number, y: number): number; }; const add: Add = (x, y) => x + y;
関数から関数の型を取得
TypeScript の typeof 型演算子を使って、関数の実装から関数の型を宣言できます。
以下の typeof は変数から型を抽出する TypeScript の型演算子です。
// 関数の実装(関数式) const add = (x: number, y: number):number => { return x + y; }; // 上記の実装から関数の型を取得して宣言 type Add = typeof add; // 型注釈に上記で宣言した型を使用 const addDouble : Add = (x, y) => { return (x + y) *2; } console.log(addDouble(1,2)); // 6
以下でも同じです。
// 関数の実装(関数宣言) function add(x: number, y: number):number { return x + y; } // 上記の実装から関数の型を取得して宣言 type Add = typeof add;
上記の typeof は JavaScript の typeof 演算子とは別物です。
JavaScript の typeof 演算子はオペランドの型を示す文字列を返します。
function add(x: number, y: number):number { return x + y; } // type 文の右辺の式で使う場合は TypeScript の typeof 型演算子 type Add = typeof add; // 以下は JavaScript の typeof 演算子 const addType = typeof add; console.log(addType); // function console.log(typeof 1); // number
関数のシグネチャ(用語)
TypeScript における「シグネチャ」は、主に関数やメソッドの型情報を表すもの(型のこと)です。
関数型の定義にはいくつかの部分があり、関数のパラメータの型と戻り値の型を指定する部分がシグネチャ(signature)と呼ばれます。
関数のシグネチャは、その関数がどのようなパラメータを受け取り、どのような値を返すかに関する情報を示す部分です。シグネチャには関数のパラメータの型、引数の数、および戻り値の型が含まれます。
以下が基本的な関数シグネチャの要素です。
- パラメータの型: 関数が受け取る引数の型を指定します。例えば、(x: number, y: string)というシグネチャは、数値型の x と文字列型の y を引数として受け取る関数を示します。
- 引数の数: シグネチャにおいて引数の数も重要です。関数が受け取る引数の数がシグネチャと一致しない場合、型エラーが発生します。
- 戻り値の型: 関数が返す値の型を指定します。例えば、: numberという記述は、関数が数値を返すことを示します。
以下の関数のシグネチャは(x: number, y: number): number
になります。これは「addという関数は、number 型の x と y という2つのパラメータを受け取り、number型の値を返す」という意味になります。
function add(x: number, y: number): number { return x + y; } // シグネチャ:(x: number, y: number): number // シグネチャの要素: // - パラメータの型: number, number // - 引数の数: 2 // - 戻り値の型: number
以下の関数型の定義の場合は(x: number, y: string) => boolean
が関数型のシグネチャです(型エイリアスが関数のシグネチャを表しています)。
type MyFunctionType = (x: number, y: string) => boolean; // シグネチャ:(x: number, y: string) => boolean // シグネチャの要素: // - パラメータの型: number, string // - 引数の数: 2 // - 戻り値の型: boolean
コールシグネチャ
コールシグネチャ(Call Signature)はオブジェクト型の定義の一部で、定義したオブジェクトが関数として呼び出せるかどうかを示します。以下はインターフェースを使った例です。
// インターフェース内の関数のシグネチャ interface AddFunction { (x: number, y: number): number; // コールシグネチャ } // 上記のように定義した AddFunction 型のオブジェクトは関数として呼び出せる const add: AddFunction = (x, y) => { return x + y; }; console.log(add(3,4)); // 7
type 文を使っても同様に記述できます。
コールシグネチャは、オブジェクトが関数として呼び出せるかどうかを定義するもので、書き方が異なりますが、基本的には type 文を使った関数型としてのシグネチャと同じです。
type AddFunction = { (x: number, y: number): number; // コールシグネチャ } // type 文を使った関数のシグネチャと同じこと // type AddFunction = (x: number, y: number) => number; const add: AddFunction = (x, y) => { return x + y; }; console.log(add(3,4)); // 7
メソッドシグネチャ
以下はメソッドシグネチャの例です。
5行目がメソッドシグネチャです。コールシグネチャ(関数の型定義)のように見えますが、calc という名前が定義されているのが、コールシグネチュアと異なります。
このメソッドシグネチャが定義されたインタフェースをオブジェクトの型定義として指定した場合、その名前の関数(メソッド)をプロパティとして記述する必要があります。
// インタフェースを使ったオブジェクトの型定義 interface CalcTwo { name: string; // calc という名前が定義されている calc(x: number, y: number): number; // メソッドシグネチャ }
上記の CalcTwo 型のオブジェクトは必ず calc メソッドを実装していることを意味します。
// CalcTwo 型のオブジェクト const addTwo: CalcTwo = { name: "足し算", calc: function(x, y) { return x + y; } } const result1 = addTwo.calc(3,4); console.log(`${addTwo.name}の結果:${result1}`); // 足し算の結果:7 // CalcTwo 型のオブジェクト const multiplyTwo: CalcTwo = { name: "掛け算", calc: function(x, y) { return x * y; } } const result2 = multiplyTwo.calc(3,4); console.log(`${multiplyTwo.name}の結果:${result2}`); // 掛け算の結果:12
オーバーロードシグネチャ
以下はオーバーロード関数の例ですが、オーバーロード関数は関数シグネチャ(オーバーロードシグネチャ)と実装の2つの部分に分けて書きます。
function greet(name: string): string; // オーバーロードシグネチャ(関数シグネチャ) function greet(age: number): string; // オーバーロードシグネチャ(関数シグネチャ) // 関数の実装部分 function greet(value: string | number): string { if (typeof value === 'string') { return `Hello, ${value}!`; } else { return `Hello, ${value} years old person!`; } } console.log(greet('foo')); // Hello, foo! console.log(greet(4)); // Hello, 4 years old person!
プロパティシグネチャ
プロパティシグネチャはインターフェース内で使用され、オブジェクトのプロパティの型を定義します。例えば、以下の Person インターフェースは name と age というプロパティの型情報を提供します。
interface Person { name: string; age: number; }
プロパティのシグネチャにはインデックスシグネチャもあります。
関数型の部分型関係
オブジェクトの型に部分型関係があるのと同様、関数型にも部分型関係があります。
関数型の間に発生する部分型関係は戻り値の型によるものや引数の型や数によるものがあります。
戻り値の型による部分型関係
型 S が型 T の部分型であれば(同じ引数を持っている場合)、S 型を返す関数型は T 型を返す関数型の部分型になります。
S が T の部分型であれば、S 型の値を T 型の値の代わりに使えるので、S 型の値を返す関数は T 型の値を返す関数の代わりに使えることになります。
以下は関数型の部分型関係において、戻り値の型が部分型関係にある場合の例です。
AdvancedObject は BasicObject の部分型という関係にあります。そして関数型 BasicFunction 型と AdvancedFunction 型を定義しています。
BasicFunction は BasicObject 型のオブジェクトを返す関数で、そのオブジェクトには x というプロパティがあり、AdvancedFunction はより多くの情報を持つオブジェクトを返す関数です。
この場合、関数型 AdvancedFunction は 関数型 BasicFunction の部分型になります。
関数型の戻り値の型が上記の部分型関係にあるため、BasicFunction 型の変数には、同じまたはより詳細な(拡張した)型のオブジェクトを返す関数を代入することができます。
// BasicObject と AdvancedObject という2つの型を定義 type BasicObject = { x: number; } // AdvancedObject は BasicObject の部分型 type AdvancedObject = { x: number; y: string; } // 基本的な関数型 type BasicFunction = () => BasicObject; // 戻り値の型が部分型関係にある関数型 type AdvancedFunction = () => AdvancedObject; // AdvancedFunction 型の関数を定義して変数に代入 const myFunction: AdvancedFunction = () => ({ x: 1, y: 'a' }); // BasicFunction 型の変数に AdvancedFunction 型の変数を代入できる const basicFunction: BasicFunction = myFunction; // AdvancedFunction 型は BasicFunction 型の部分型である // 変数 result は BasicObject 型のオブジェクト const result: BasicObject = basicFunction(); // result は x と y プロパティを持つオブジェクト console.log(result); // {x: 1, y: 'a'}
上記の場合、AdvancedFunction 型の変数 myFunction は、x と y の2つのプロパティを持つオブジェクトを返しますが、それを BasicFunction 型の変数 basicFunction に代入できます。
また、変数 result は BasicObject 型のオブジェクトですが、myFunction を代入した関数 basicFunction は x と y プロパティを持つオブジェクトを返します。
逆に AdvancedFunction 型に BasicFunction 型を代入しようとすると「型 'BasicFunction' を型 'AdvancedFunction' に割り当てることはできません。」というコンパイルエラーになります。
// BasicFunction 型の関数
const myBasicFunction: BasicFunction = () => ({ x: 1});
// 以下はコンパイルエラー
const myAdvancedFunction: AdvancedFunction = myBasicFunction;
// error TS2322: Type 'BasicFunction' is not assignable to type 'AdvancedFunction'.
void 型を返す関数型の部分型
どのような型を返す関数も、同じ引数を受け取って void 型を返す関数型の部分型として扱われます。
void 型は戻り値がない関数を表す型なので、どのような型を返す関数でも(同じ引数を受け取る関数であれば)戻り値がない関数の代わりに使うことができます。
以下の場合、関数 func は (name: string) => string 型の関数ですが、 (name: string) => void 型の関数に代入することができます。
const func = (name: string) => 'hello ' + name; const v :(name: string) => void = func; console.log(v('foo')); // hello foo
引数の型による部分型関係
型 S が型 T の部分型であれば、T 型を引数に受け取る関数の型は、S 型を引数に受け取る関数の部分型になります(※ 先述の戻り値の型による部分型関係と部分型関係が逆転しています)。
以下は関数型の部分型関係において、引数の型が部分型関係にある場合の例です。
この例の場合、AdvancedObject 型は BasicObject 型の部分型です。
BasicObject を引数に取る関数の型は、AdvancedObject を引数に取る関数の型の部分型になります。
つまり、BasicFunction 型は AdvancedFunction 型の部分型です。
そのため、myFunction が BasicFunction 型であるとき、それを AdvancedFunction 型の変数である advancedFunctionに 代入できます。
// BasicObject と AdvancedObject という2つの型を定義 type BasicObject = { x: number; } // AdvancedObject は BasicObject の部分型 type AdvancedObject = { x: number; y: string; } //上記の型に基づいてオブジェクトを作成 const basicObj = { x: 1 } const advancedObj = { x: 1, y: 'a' } // 引数が部分型関係にある2つの関数型を定義 type BasicFunction = (obj: BasicObject) => void; type AdvancedFunction = (obj: AdvancedObject) => void; // 引数に BasicObject 型を受け取る BasicFunction 型の関数を定義 const myFunction: BasicFunction = (obj: BasicObject) => { console.log(obj.x); }; // BasicFunction 型の関数 myFunction を AdvancedFunction 型の変数に代入可能 const advancedFunction: AdvancedFunction = myFunction; advancedFunction(advancedObj); // 1
逆に AdvancedFunction 型の関数を BasicFunction 型の変数に代入しようとするとコンパイルエラーになります。
// AdvancedFunction 型の関数
const myAdvancedFunction: AdvancedFunction = (obj: AdvancedObject) => {
console.log(obj.x, obj.y);
};
myAdvancedFunction(advancedObj); // 1 'a'
// 以下はコンパイルエラー
const myBasicFunction: BasicFunction = myAdvancedFunction;
// error TS2322: Type 'AdvancedFunction' is not assignable to type 'BasicFunction'.
引数の数による部分型関係
引数の数の違いによっても関数型の間に部分型関係が発生します。
以下の例では、BasicFunction 型と AdvancedFunction 型を定義しています。BasicFunction は単一の引数を取る関数型で、AdvancedFunction は2つの引数を取る関数型です。
この時、BasicFunction は AdvancedFunction の部分型になり、BasicFunction を AdvancedFunction の代わりに使うことができます。
// 基本的な関数型 type BasicFunction = (x: number) => void; // 引数の個数が部分型関係にある関数型(BasicFunction は AdvancedFunction の部分型) type AdvancedFunction = (x: number, y: string) => void; // BasicFunction 型の関数を定義して変数に代入 const myFunction: BasicFunction = (x) => { console.log(x); }; // AdvancedFunction 型の変数に BasicFunction 型の変数(関数)を代入できる const advancedFunction: AdvancedFunction = myFunction; advancedFunction(1,'a'); // 1
上記の advancedFunction は AdvancedFunction なので2つの引数を渡していますが、代入した関数 myFunction の定義では引数を1つしか受け取りません。そのため、1番目の引数は出力されますが、2つ目の引数は使われません。
逆に AdvancedFunction 型の関数を BasicFunction 型の変数に代入しようとするとコンパイルエラーになります。
const myAdvancedFunction: AdvancedFunction = (x,y) => {
console.log(x, y);
}
myAdvancedFunction(1,'a'); // 1 'a'
const myBasicFunction: BasicFunction = myAdvancedFunction;
// error TS2322: Type 'AdvancedFunction' is not assignable to type 'BasicFunction'.
ジェネリクス
ジェネリクスは、型も変数のように扱えるようにすることで、コードの共通化を可能にして再利用性を高め、型安全性を向上させるための機能です。
例えば、以下の関数は引数として受け取った値をそのまま返すだけのものですが、引数と戻り値の型を number としているため、文字列を引数に渡すとコンパイルエラーになります。
function identity(arg: number): number { return arg; } console.log(identity(1)); // 1 console.log(identity('001')); // コンパイルエラー
引数に文字列を渡したい場合は、以下のように別の関数を作成することもできます。
function identityStr(arg: string): string { return arg; } console.log(identityStr('001')); // 001
上記のように、それぞれのデータ型に対して別々の関数を書くこともできますが、効率的ではありません。
このような場合、ジェネリクスを使うと柔軟な関数を作成することができます。
以下は上記の2つの関数をジェネリクスを使って1つの関数に書き換えたものです。
ジェネリクスでは関数名の後に <T>
のように型パラメータを追加します。
型パラメータは型引数(この例では T)を< >
で囲みます。型引数の名前は一般的に T がよく使われますが、任意の文字列(有効な識別子)を名前として使用できます。
function identity<T>(arg: T): T { return arg; }
関数の引数の型や戻り値の型として書かれたT
(型変数)は型引数を参照し、関数の中でも使用できます。
型引数(型変数)T は、関数が呼び出されたときに渡される実際の型のプレースホルダとして機能します。
関数の呼び出し時には T に任意の型を入れることができます。宣言と同じように < >
で括られている中に型名を明示的に書くことで指定できます。 また、型推論も可能で、多くの場合、型パラメータを省略することができます。
例えば、この例の場合、型引数に number を渡すと、関数は number 型の引数を受け取り、number 型の値を返します。同様に string 型を渡すと、string 型の引数を受け取り、string 型の値を返します。
// 型引数に number を指定し、引数に数値を渡す(result1 は number 型) const result1 = identity<number>(1); // 型引数に string を指定し、引数に文字列渡す(result2 は string 型) const result2 = identity<string>('001');
このようにジェネリクスを使った関数は、呼び出すたびに異なる型引数を与えることができます。
VS Code を使用している場合、関数名の部分にマウスオーバーすると指定された型引数での関数の型が表示されます。
呼び出す側では型パラメータは省略可能
TypeScript は型推論も行うので、型パラメータを省略することもできます。
多くの場合、型引数を持つ関数を呼び出す側では型パラメータは省略することができます。
const result3 = identity(3); // 型パラメータを省略(型推論が行われる)
呼び出す側では必要に応じて型パラメータを指定することで型を限定することができます。
const result4 = identity<string>(4); // 型引数と異なるのでコンパイルエラー // Argument of type 'number' is not assignable to parameter of type 'string'.
以下は、ジェネリクスを使って配列の中身を逆順にする関数の例です。
型引数で指定した T 型の要素を持つ配列を受け取り、その配列を逆順にした T 型の配列を返します。
function reverseArray<T>(array: T[]): T[] { return array.reverse(); } const reversedNumbers = reverseArray([1, 2, 3]); // 数値の配列(型パラメータを省略) console.log(reversedNumbers); // [3, 2, 1] const reversedStrings = reverseArray(['a','b','c']); //文字列の配列(型パラメータを省略) console.log(reversedStrings); // ['c', 'b', 'a']
以下は T 型の引数 value を n 回繰り返して作成した T 型の配列を返すジェネリック関数の例です。
function repeatIt<T>(value: T, n: number): T[] { const result:T[] = []; for(let i=0; i < n; i++) { result.push(value); } return result; } console.log(repeatIt('hello', 3)); // ['hello', 'hello', 'hello'] console.log(repeatIt(7, 4)); // [7, 7, 7, 7]
少し複雑な例
以下はジェネリクスを使ってオブジェクトの配列を条件に基づいてフィルタリングする関数の例です。
関数 filterItems は第1引数に T 型のオブジェクトの配列を、第2引数に配列のメソッド filter() に渡すコールバック関数を受け取ってフィルタリングした結果の配列を返します。
第2引数に渡すコールバック関数は T 型のオブジェクトを受け取り真偽値を返します。
function filterItems<T>(items: T[], callback: (item: T) => boolean): T[] { return items.filter(callback); }
以下は filterItems の第1引数に User 型オブジェクトの配列を、第2引数に User 型オブジェクトの age プロパティの値が30未満の場合に true を返すコールバック関数を指定して、age プロパティの値が30未満のオブジェクトを抽出する例です(関連サンプル)。
type User = { id: number; name: string; age: number; } // User 型の配列 const users: User[] = [ { id: 1, name: 'Foo', age: 22 }, { id: 2, name: 'Bar', age: 33 }, { id: 3, name: 'Baz', age: 44 } ]; const youngUsers = filterItems(users, user => user.age < 30); console.log(youngUsers); // [ {id: 1, name: 'Foo', age: 22} ]
同じ関数を使って以下のように異なる型のオブジェクトの配列をフィルタリングすることもできます。
type Fruits = { name: string; color: string; } const fruits: Fruits[] = [ {name: 'apple', color:'green'}, {name: 'melon', color: 'green'}, {name: 'strawberry', color: 'red'} ]; const redFruits = filterItems(fruits, fruit => fruit.color === 'red'); console.log(redFruits); //[ {name: 'strawberry', color: 'red'} ]
型引数の宣言方法
これまでのジェネリクスの例では関数宣言を使いましたが、関数式やアロー関数、メソッド記法でも型引数を宣言することができます。
以下は関数宣言を使った例です。関数名(identity)の後に型パラメータ <T>
を追加します。
function identity<T>(arg: T): T { return arg; }
関数式
以下は関数式を使った記述例です。
const identity = function identity<T>(arg: T): T { return arg; }
実際には以下のように式の部分の関数名を省略することがほとんどですが、その場合は、function の後に型パラメータを記述します。
const identity = function<T>(arg: T): T { return arg; }
アロー関数
以下はアロー関数での記述例です。引数の前に型パラメータを記述します。
const identity = <T>(arg: T): T => { return arg; }
メソッド
以下はオブジェクトのメソッド(プロパティ)としての記述例です。
const obj = { identity: function <T>(arg: T): T { return arg; } } console.log(obj.identity('obj')); // アロー関数を使って記述 const obj2 = { identity: <T>(arg: T): T => { return arg; } } // メソッドの短縮記法 const obj3 = { identity <T>(arg: T): T { return arg; } }
複数の型引数
複数の型引数を持つジェネリクスも定義できます。
以下は2つの異なる型引数を持つジェネリクス関数の例です
この関数は、T 型と U 型の二つの引数を受け取り、それらを持つタプル(要素数が固定された配列)を返します。例えば、number 型と string 型の引数を渡すと、それぞれの型を持つタプルが返されます。
function pair<T, U>(x: T, y: U): [T, U] { return [x, y]; } const numStr = pair<number, string>(1, "one"); console.log(numStr); // [1, 'one']
型引数の制約
型引数に extends キーワードを用いることで、何らかの特定の型を受け入れるように型引数に制約をつける(型の範囲を制限する)ことができます。
以下の例では、printValue 関数は型引数 T を取り、その型が number または string であることを要求しています。extends キーワードは、型引数が指定した型(またはその部分型)に制限します。
function printValue<T extends number | string>(value: T): void { console.log(value); } printValue(5); // 5(number 型なのでOK) printValue("hello"); // hello(string 型なのでOK) printValue(true); // コンパイルエラー(boolean 型は許可されていない)
次の例では、Lengthy というオブジェクト型のエイリアスを定義しています。そして、logLength 関数では、型引数 T が Lengthy 型を満たす型(length プロパティを持つ型)であることを要求しています。
type Lengthy = { length: number; } function logLength<T extends Lengthy>(arg: T): void { console.log(arg.length); }
これにより、logLength 関数は Lengthy 型を満たす型(length プロパティを持つ型)の引数に対してのみ使用できます。
logLength("Hello"); // 5 (string は length プロパティを持つので OK) logLength([1, 2, 3]); // 3 (配列も length プロパティを持つので OK) logLength(42); // コンパイルエラー, number 型は length プロパティを持たない
以下は keyof と extends を使ってオブジェクトの配列のプロパティのキーをフィルタリングする例です。例えば、以下のようなオブジェクト型がある場合、
type User = { id: number; name: string; age: number; }
以下の関数 filterProp を使うと、特定のプロパティに基づいて上記のようなオブジェクトの配列をフィルタリングすることができます。
関数 filterProp では <T, K extends keyof T>
のように2つの型引数 T と K を指定しています。
T は配列内のオブジェクトの型を表し、K はフィルタリングしたいオブジェクトのプロパティのキーを表します。keyof はオブジェクトの型からプロパティ名を型として返す型演算子です。
extends を使うことによって、K 型パラメータが T 型の有効なキーであることを制約しています。
function filterProp<T, K extends keyof T>(items: T[], prop: K, value: T[K]): T[] { return items.filter(item => item[prop] === value); }
上記の関数はコンパイルすると以下のような JavaScript になります。
function filterProp(items, prop, value) { return items.filter(item => item[prop] === value); }
関数 filterProp は、第1引数に T 型のオブジェクトの配列を、第2引数にプロパティ名(プロパティのキー)を、第3引数に値(T 型オブジェクトのプロパティの値)を受け取ります。
そして配列のメソッド filter を使って、第2引数に指定されたプロパティの値が第3引数に指定された値と一致したオブジェクトの配列を返します。
extends を使って K 型パラメータが T 型の有効なキーであることを制約しているので、第2引数のプロパティ名にオブジェクトの有効なキー以外を指定するとコンパイルエラーになります。
type User = { id: number; name: string; age: number; } function filterProp<T, K extends keyof T>(items: T[], prop: K, value: T[K]): T[] { return items.filter(item => item[prop] === value); } // User 型の配列 const users: User[] = [ { id: 1, name: 'Foo', age: 22 }, { id: 2, name: 'Bar', age: 33 }, { id: 3, name: 'Baz', age: 44 } ]; const baz = filterProp(users, 'name', 'Baz'); const id2 = filterProp(users, 'id', 2); console.log(baz); // [ {id: 3, name: 'Baz', age: 44} ] console.log(id2); // [ {id: 2, name: 'Bar', age: 33} ] // 存在しないキー(id、name、age 以外)を指定するとコンパイルエラーになる const year = filterProp(users, 'year', 1990); //year によりコンパイルエラー
以下は、extends を使って、引数の型 T が必ず HTMLElement またはその部分型の HTMLButtonElement や HTMLDivElement であることが保証して style プロパティに安全にアクセスできるようにする例です。
単に T 型を指定しただけでは、任意の型が指定可能なので、渡す型によっては style プロパティが存在しない可能性があるためコンパイルエラーになります。
function changeColor<T extends HTMLElement>(element: T) { element.style.color = "red"; return element; } const target = document.getElementById('target'); // 要素が取得できていれば、関数を実行して文字を赤色に if(target != null) { changeColor(target); }
型引数を持つ関数型の宣言
通常の関数の型宣言と同様、型引数を持つ関数の型を宣言(定義)することができます。
以下は引数に受け取った T 型の値を返すジェネリック関数です。
const identity = function<T>(arg: T): T { return arg; }
上記の関数 identity の関数型は type 文を使って以下のように定義することができます。型パラメータが最初に追加され、引数や戻り値の型の部分は必要に応じて型引数が使えます。
type Identity = <T>(arg: T) => T; // 定義された関数型を型注釈に使用(引数と戻り値の型注釈は省略) const identity: Identity = function(arg) { return arg; }
通常の関数同様、VS Code などで関数が代入された変数名 identity にマウスオーバーすると関数の型が表示されます。ジェネリック関数の関数型は、先頭に型パラメータが加えられています。
interface を使って以下のように定義することもできます。type 文の=>
が:
に代わっています。
interface Identity { <T>(arg: T): T; }
同様に以下のジェネリック関数の関数型は、
const repeatIt = <T>(value: T, n: number): T[] => { const result:T[] = []; for(let i=0; i < n; i++) { result.push(value); } return result; }
type 文を使って以下のように定義することができます。
type RepeatIt = <T>(value: T, n: number) => T[]; const repeatIt: RepeatIt = (value, n: number) => { const result = []; for(let i=0; i < n; i++) { result.push(value); } return result; }
interface を使って以下のように定義することもできます。
interface RepeatIt { <T>(value: T, n: number) : T[]; }
コールバック関数を受け取る関数の作成
以下はコールバック関数を受け取る簡単なサンプルです。
コールバックを受け取る関数 someFunc は何らかの処理をして(この場合は乱数を生成)1秒後にコールバック関数を呼び出します。
// コールバック関数の型定義 type MyCallBack = (result: number) => void; // コールバックを受け取る関数 function someFunc(callback: MyCallBack) { // 何らかの処理(値を生成して1秒後にコールバックを呼び出す) setTimeout(() => { const result = Math.round(Math.random() * 10); // コールバックに渡す値 callback(result); // コールバックを呼び出す }, 1000); } // コールバック関数の定義 function handleResult(result: number) { console.log(`操作が完了しました。結果は ${result} です。`); } // 関数の呼び出し(引数にコールバックを渡す) someFunc(handleResult);
上記では、type 文でコールバック関数の型を別途定義していますが、以下のように引数の中に直接記述することもできます。
// コールバック関数の型を直接引数に指定 function someFunc(callback: (result: number) => void) { setTimeout(() => { const result = Math.round(Math.random() * 10); callback(result); }, 1000); }
また、上記ではコールバック関数を別途定義していますが、このような単純な関数の場合、アロー関数を使って直接引数に無名関数として渡すこともできます。
// 関数呼び出しで無名関数としてコールバックを指定 someFunc((result: number) => console.log(`操作が完了しました。結果は ${result} です。`));
したがって最初のサンプルは以下のように記述することもできます。
function someFunc(callback: (result: number) => void) { setTimeout(() => { const result = Math.round(Math.random() * 10); callback(result); }, 1000); } someFunc((result: number) => console.log(`操作が完了しました。結果は ${result} です。`));
以下はコールバックの戻り値を任意の型にできるようにジェネリクスを使った書き換えた例です。
function someFunc<T>(callback: (rand: number) => T) { const rand = Math.round(Math.random() * 10); // result は T 型 const result = callback(rand); return result; } // 戻り値が boolean 型のコールバックを指定 const resultBool = someFunc((rand: number) => rand > 5); console.log(resultBool); // 乱数の値が5より大きければ true そうでなければ false // 戻り値が number 型のコールバックを指定 const resultNumber = someFunc((rand: number) => rand * 2); console.log(resultNumber); // 乱数の2倍の値 // 戻り値が void 型のコールバックを指定(someFunc 内での変数 result は undefined になります) someFunc((result: number) => console.log(`乱数の値は ${result} です。`));
配列とコールバック関数を受け取る独自 map 関数
以下の独自関数 map は number 型の配列とコールバック関数を引数に受け取ります。そして配列の各要素をコールバック関数で処理して number 型の配列を作成して返します。
第1引数 arr に number 型の配列を受け取り、第2引数 callback に配列の要素である number 型の引数 val を受け取り number 型の値を返すコールバック関数を受け取ります。
関数の本体では、第1引数 arr に受け取った配列の各要素を for of 文で第2引数に受け取ったコールバック関数 callback に渡し、その戻り値を結果の配列に入れています。
戻り値はこの場合、number 型の配列になります。
function map(arr: number[], callback:(val: number) => number): number[] { // 戻り値として返す number 型の配列の初期化 const result: number[] = []; // 配列の各要素を for of 文でコールバック関数に渡して戻り値を配列に追加 for(const elem of arr) { result.push(callback(elem)); } // 作成した配列を返す return result; } const data = [1, 2, 3, 4]; const result = map(data, (x)=> x*2); console.log(result); // [2, 4, 6, 8]
以下のようにコールバック関数の定義及びその型の定義を別途記述しても同じです。
// コールバック関数の型定義 type MyCallBack = (val: number) => number; // コールバック関数 const double: MyCallBack = (x)=> x*2; function map(arr: number[], callback: MyCallBack): number[] { const result: number[] = []; for(const elem of arr) { result.push(callback(elem)); } return result; } const result = map( [1, 2, 3, 4], double); console.log(result); // [2, 4, 6, 8]
for 文を使う場合
上記の場合、配列の各要素の処理は for of 文を使っていますが、for 文を使って以下のように記述すると4行目で typeof arr[i] === "number"
で値をチェックしてもコンパイルエラーになります。
function map(arr: number[], callback:(val: number) => number): number[] {
const result: number[] = [];
for(let i=0; i<arr.length; i++) {
if (typeof arr[i] === "number"){
result.push(callback(arr[i])); // コンパイルエラー
//'number | undefined' の引数を 'number' のパラメーターに割り当てることはできません。
}
}
return result;
}
コンパイルエラーを回避するには、arr[i]
を一度変数に代入して、その変数が nummber 型であること(または undefined でないこと)を確認します。
function map(arr: number[], callback:(val: number) => number): number[] { const result: number[] = []; for(let i=0; i<arr.length; i++) { const val = arr[i]; if (typeof val === "number"){ // または if(val !== undefined) result.push(callback(val)); } } return result; }
ジェネリクスを使う
前述の関数は number 型の配列を引数に受け取りますが、以下はジェネリクスを使ってどのような型の配列でも受け取れるように書き換えたものです。
この例では、受け取る配列と返す配列が異なっていても良いように2つの型引数 T と U を持たせています。
受け取る配列 arr の要素の型を T 型としているのでコールバック関数に受け取る値の型は T 型になります。そして、コールバック関数が返す値の型を U 型としているので、map の戻り値は U 型の配列になります。
T 型と U 型が同じ型であっても問題ありません。
function map<T, U>(arr: T[], callback:(val: T) => U): U[] { const result: U[] = []; for(const elem of arr) { result.push(callback(elem)); } return result; } const data = ['hello','foo','goodbye!']; // 配列の要素の文字列の長さ(数値)の配列を作成 const result1 = map(data, (x)=> x.length); console.log(result1); // [5, 3, 8] // 配列の要素の文字列の長さが3より大きいかどうかの真偽値の配列を作成 const result2 = map(data, (x)=> x.length > 3); console.log(result2); // [true, false, true] // 配列の要素の文字列を大文字に変換した配列を作成 const result3 = map(data, (x)=> x.toUpperCase()); console.log(result3); // ['HELLO', 'FOO', 'GOODBYE!']
例外処理
JavaScript や TypeScript では、以下のように存在しない関数を呼び出すと例外が発生します。
例外とはランタイムエラーのことで、以下の場合は定義されていない関数を呼び出しているので、その部分で Uncaught ReferenceError というエラー(例外)が発生し、そこでプログラムは終了します。
console.log('start'); // start と出力される // 未定義の関数の呼び出し notFunction(); // Uncaught ReferenceError(例外が発生してプログラムは終了) console.log('end'); // 実行されない
また、throw 文を使って例外を発生させることができます。
以下の関数 printHello() は文字列を受け取りますが、文字数が0であれば例外を発生させます。
そのため、1行目の printHello() の呼び出しでは Hello, foo! とコンソールに出力されますが、2行目の呼び出しでは受け取った文字数が0なので例外が発生し、それ以降の処理は実行されません。
printHello('foo'); printHello(''); // 例外発生 console.log('終了'); // 実行されない function printHello(str: string): void { if(str.length === 0){ throw new Error("文字数が0です"); // 文字数が0の場合は例外を発生 } console.log(`Hello,${str}!`); }
コンパイルしてブラウザのコンソールで確認すると以下のように出力されます。
printHello() のthrow new Error("文字数が0です")
により、エラーメッセージ「Uncaught Error: 文字数が0です」が2行目に出力されています(Uncaught Error は捕捉されていないエラーという意味)。
3〜4行目はブラウザが出力するエラーの情報(スタックトレース)で、例外が発生した場所とそのコードの呼び出し元が出力されています(この場合の行番号はコンパイル後の JavaScript のものです)。
また、例外が発生するとプログラムは強制終了するので、console.log('終了') は実行されず「終了」は出力されていません。
Hello,foo! Uncaught Error: 文字数が0です at printHello (index.js:7:15) at index.js:3:1
try...catch 構文
例外が発生しても強制終了しないようにするには try...catch 構文を使って例外を捕捉(catch)します。
try...catch 構文の try ブロック内で例外が発生すると、try ブロック内のそれ以降の処理は実行されず、try ブロックで発生したエラーオブジェクト(error)とともに catch ブロックが呼び出されます。
try { // try ブロック } catch (error) { // catch ブロック(エラーオブジェクト error を受け取る) }
また、必要に応じて try ブロック内で例外が発生したかどうかに関わらず、必ず try 文の最後に実行される finally ブロックを付け加えることができます。
try { // try ブロック } catch (error) { // catch ブロック } finally { // finally ブロック(このブロックは例外の発生に関係なく必ず実行される) }
以下は先述の例を try...catch 構文を使って書き換えたものです。
try...catch 構文は、try ブロックの中の処理を実行し、例外が発生するとそれ以降の処理は中断し(強制終了はせずに)即座に catch ブロックを実行します。
catch ブロックを実行する際には、throw 文で投げられたエラーオブジェクトが catch (error) で宣言された変数 error に入ります。変数名の error は任意の文字列で、この変数は error ブロックの中でのみ参照できます。
try...catch 構文を使うと、try ブロックの中で例外が発生しても、発生した例外を捕捉することで強制終了を回避できます。そのため、以下の10行目の console.log('終了') は実行されます。
try { printHello('foo'); // 実行される printHello(''); // 例外が発生するので catch ブロックに移行 console.log('これは出力されない'); // 実行されない } catch (error) { // 発生したエラーオブジェクト(error)をエラーとして出力 console.error(error); } console.log('終了'); // 例外は捕捉されたので実行される function printHello(str: string): void { if(str.length === 0){ throw new Error("文字数が0です"); } console.log(`Hello,${str}!`); }
コンパイルしてブラウザのコンソールで確認すると以下のように出力されます。
Hello,foo! // printHello('foo') の実行結果(try ブロック) Error: 文字数が0です // printHello('') で発生する例外のエラーメッセージ(catch ブロック) at printHello (index.js:14:15) at index.js:4:5 終了 // console.log('終了') の実行結果
catch に渡される変数の型
TypeScript では catch に渡される変数の型はコンパイラオプションで strict が有効の場合は unknown 型になり、無効の場合は any 型になります。
strict が true の場合、useUnknownInCatchVariables が true になるため、catch に渡される変数の型は unknown 型になります(バージョン 4.4 から)。
そのため、以下のようにエラーオブジェクトの message プロパティにアクセスすると unknown 型の変数には型の確証がないため、そのメンバーにアクセスすることは許可されず、コンパイルエラーになります。
try { throw new Error("something wrong"); } catch (e) { console.log(e.message); // コンパイルエラー // 'e' is of type 'unknown'. }
この場合、以下のように e instanceof Error のチェックを行ってから e.message にアクセスすれば、TypeScript はそのブロック内で e を Error クラスとして扱うので、コンパイルエラーになりません。
try { throw new Error("something wrong"); } catch (e) { if (e instanceof Error) { console.error(e.message); } }
独自例外クラスの作成
独自の例外クラスを作成することもできます。プロジェクト内で共通のエラーハンドリング(例外処理)を実装する際に有用です。
Error クラスを継承することで独自のクラスを作成できます。以下は CustomError という名前の独自の例外クラスを作成して使用する例です。
CustomError クラスの定義では Error クラスを継承してコンストラクタをオーバーライドして name プロパティを設定しています。Error オブジェクトのプロパティには message や name があります。
以下では確認のために CustomError クラスの例外を投げる関数 throwCustomError() を作成して try ブロックで例外を発生させています。
throwCustomError() の戻り値の型を never 型にしているのは、その関数が例外を投げて終了することを示しています(省略すると型推論により void 型になります)。
catch ブロックでは、受け取ったエラーが CustomError のインスタンスかどうかを instanceof 演算子で判定しています。
// Error クラスを継承して独自の例外クラスを定義 class CustomError extends Error { // コンストラクタのオーバーライド constructor(message: string) { // 親クラスのコンストラクタの呼び出し super(message); // name プロパティを設定 this.name = "CustomError"; } } // CustomError の例外を投げる関数 function throwCustomError(): never { throw new CustomError("This is a custom error"); } try { // 例外が発生する可能性があるコード throwCustomError(); // 確認のため例外を発生させる } catch (error) { if (error instanceof CustomError) { // 例外が CustomError の場合の処理 console.error("Custom error caught:", error.message); } else if (error instanceof Error) { // 例外が CustomError 以外の場合の処理 console.error("Unexpected error:", error.message); } } // コンソールに以下が出力される // Custom error caught: This is a custom error
型ガード関数を使う
catch ブロックの instanceof 演算子での型の判定は、以下のような型ガード関数を作成して判定することもできます。
isCustomError() は引数が CustomError クラスのインスタンスであるかどうかを確認(判定)します。
型ガード関数は、戻り値の型として型述語 is
が書かれた特殊な関数で、引数名 is 型
という構文になっています。その関数が true を返すなら引数名
に与えられた値が型
であるという意味になります。
また、この例の catch ブロックの CustomError の判定の後では、単に else にしていますが、これは error のプロパティにアクセスしないので先述の例では記述していた if (error instanceof Error)
を省略しています。
class CustomError extends Error { constructor(message: string) { super(message); this.name = "CustomError"; } } function throwCustomError(): never { throw new CustomError("This is a custom error"); } // 型ガード関数 function isCustomError(error: unknown): error is CustomError { // error が CustomError かどうかの真偽値を返す return error instanceof CustomError; } try { throwCustomError(); } catch (error) { // 型ガード関数で判定 if (isCustomError(error)) { // error は CustomError である console.error("Custom error caught:", error.message); } else { // error は CustomError ではない(error をそのまま出力:プロパティにアクセスしない) console.error("Unexpected error:", error); } }