HTML5テクニカルノート
TypeScript: 関数
- ID: FN1703003
- Technique: HTML5 / JavaScript
- Package: TypeScript 2.2
TypeScript公式Handbook「Functions」をもとにした解説です。関数はJavaScriptで処理を部品分けして組み立てたり、あるいはクラスを定める役割も果たします。TypeScriptには、ECMAScript 2015(ECMAScript 6)の仕様も採り入れて、関数の型づけや参照などについて起こりがちな問題を確かめたり、コードを簡単にわかりやすく書くための機能が備わっています。
01 関数を定める
関数はfunction
キーワードのあとに名前(識別子)をつけて定めます。以下の関数(getLength())は、ふたつの引数(xとy)を2次元平面の座標として、原点(0, 0)からの距離を三平方の定理により求めて返します。関数の引数と戻り値には、コロン(:
)を添えて型づけしました(いずれもnumber
)。関数にふたつの引数を与えてつぎのように呼び出せば戻り値が得られます。呼び出しは、関数を定める前でも構いません。
console.log(getLength(3, 4)); // 5
function getLength(x: number, y: number): number { let square = x * x + y * y; return Math.sqrt(square); }
関数に名前をつけず、つぎのように変数(getLength)に納めて呼び出すこともできます。この場合、関数は代入式で定められますので、呼び出しは式よりあとでなければなりません。
let getLength = function(x: number, y: number): number { let square = x * x + y * y; return Math.sqrt(square); } console.log(getLength(3, 4)); // 5
関数本体から、外の変数を参照することもできます。ただ、このような処理は注意して使ったほうがよいでしょう。
let origin = {x: 2, y: 3}; function getLength(x: number, y: number): number { x -= origin.x; y -= origin.y; let square = x * x + y * y; return Math.sqrt(square); } console.log(getLength(3, 4)); // 1.4142135623730951
02 関数で型づけする
変数に関数の型を与えることもできます。決めるのは引数と戻り値です。引数は関数を定めるときと同じようにコロン(:
)で型づけします。そして、戻り値の型はアロー=>
のあとに添えます。戻り値がないときは、型にvoid
を定めなければなりません。
let getLength: (x: number, y: number) => number; getLength = function(x: number, y: number): number { let square = x * x + y * y; return Math.sqrt(square); };
引数は数とそれぞれの型だけが決まり、名前は問いません。
let getLength: (x: number, y: number) => number; getLength = function(start: number, last: number): number { return last - start; }
TypeScriptは型を推論で定めます。たとえば、number
で型づけした引数を数値演算して返せば、戻り値の型が省かれていてもnumber
とされるのです。型を決めていない変数に関数を納めると、その関数の型が与えられます。すると、変数に型の合わない関数を代入したときはエラーになります。また、変数が関数で型づけされていれば、引数や戻り値に型が与えられていなくても、変数に応じて定められます。
let getLength = function(x: number, y: number) { // 戻り値はnumberと推論 let square = x * x + y * y; return Math.sqrt(square); }; // getLength = function(x: number, y: number): void {} // 戻り値がnumberでないのでエラー getLength = function(start, last) { // 変数に応じて引数と戻り値が型づけされる return last - start; }
03 引数の省略とデフォルト値
関数を呼び出すときは、定められた引数が渡されなければなりません。引数の数が足りなくても、多くてもエラーを示します。
function getLength(x: number, y: number): number { let square = x * x + y * y; return Math.sqrt(square); }; // console.log(getLength(3)); // 引数が足りないのでエラー // console.log(getLength(3, 4, 5)); // 引数が多いのでエラー console.log(getLength(3, 4)); // 5
引数のあとに?
をつけると、その値は関数の呼び出しのとき省くことができます。undefined
またはnull
の値は、渡されなかったものと扱われます。省略できる引数は必須の引数のあとに置かなければ、対応が正しく決められません。引数の数があらかじめ定まらないからです。つぎの関数(getLength())は、渡した引数が2次元(ふたつ)でも3次元(3つ)でも、原点からの距離を求めて返します[*1]。
function getLength(x: number, y: number, z?: number): number { // 第3引数は省ける let square = x * x + y * y; if (z) { square += z * z; } return Math.sqrt(square); }; console.log(getLength(3, 4)); // 5 console.log(getLength(Math.SQRT1_2, Math.SQRT1_2, Math.sqrt(3))); // 2
引数に=
演算子で値を代入すると、デフォルト値の定めになります。引数が渡されない(またはundefined
かnull
の)ときは、その値が用いられるのです。つぎの関数(getAngle())は、はじめのふたつの引数(xとy)の2次元平面座標がx軸正方向となす角度を返します。第3引数にtrue
を渡せば、戻り値は度数角です。デフォルト値はfalse
で、ラジアン値としました。
function getAngle(x: number, y: number, degree: boolean = false): number { let angle: number = Math.atan2(y, x); if (degree) { angle *= 180 / Math.PI; } return angle; }; console.log(getAngle(-1, 0)); // 3.141592653589793 console.log(getAngle(1, Math.sqrt(3), true)); // 59.99999999999999
引数のデフォルト値は、省略できる引数の替わりにも使えます。また、関数としての型づけで引数に?
を用いることもでき、デフォルト値が定められた引数とは型が合います。
let getAngle: (x: number, y: number, degree?: boolean) => number = function(x, y, degree = false) { let angle: number = Math.atan2(y, x); if (degree) { angle *= 180 / Math.PI; } return angle; };
デフォルト値を与えた引数は、省略できる引数とは違い、あらかじめ数が決まります。ですから、必須の引数より前に置いても、undefined
かnull
を渡して数さえ合わせれば、関数は呼び出せます。ただ、誤りを引き起こしやすくなるので、注意しましょう。
function getLength(x: number = 0, y: number): number { let square = x * x + y * y; return Math.sqrt(square); } console.log(getLength(undefined, 1)); // 1 console.log(getLength(1, undefined)); // NaN
図001■3次元空間座標の原点からの距離
04 値を配列にまとめる引数
TypeScriptでは、関数に数の決まらない引数がひとつにまとめて定められます。ECMAScript 2015にもとづく残余引数(rest parameters)です。省略記号(...
)のあとに引数を加えると、それ以降のすべての引数値を納めた配列が与えられます。ですから、残余引数は最後にひとつしか置けません。つぎの関数(callFunction())は、第1引数の関数(func)に第2引数以降(args)を渡して呼び出し、結果を返します。
function callFunction(func: Function, ...args: any[]) { let result = func.apply(null, args); return result; } console.log(callFunction(Math.atan2, Math.sqrt(3), 1) * 180 / Math.PI); // 59.99999999999999
残余引数は、関数としての型づけに用いることもできます。対応する引数はなくても空の配列が渡されるので、エラーにはなりません。
console.log(callFunction(Math.atan2, Math.sqrt(3), 1) * 180 / Math.PI); // 59.99999999999999 let getLength: (...numbers: number[]) => number = function(...coords) { let squaredSum: number = 0; for (let i: number = 0; i < coords.length; i++) { squaredSum += coords[i] * coords[i]; } return Math.sqrt(squaredSum); } console.log(getLength(Math.SQRT1_2, Math.SQRT1_2, Math.sqrt(3))); // 2 console.log(getLength()); // 0
JavaScriptで関数を呼び出すと、すべての引数値を配列のようにまとめたarguments
オブジェクトが本体の中でつくられます。残余引数と違うのはつぎの点です。
arguments
オブジェクトは関数に渡されたすべての引数値を納めます。残余引数は定められた位置以降の引数値を含む配列です。- 残余引数は
Array
インスタンスです。arguments
オブジェクトは配列のように値が取り出せるものの、Array
メソッドがすべて使えるわけではありません。 arguments
オブジェクトは配列にないプロパティなどの機能を備えます。
05 関数本体のthis参照
JavaScriptにおける関数本体から参照したthis
は、呼び出されるときに関連づけられたオブジェクトになります。オブジェクトのメソッドとして呼び出されれば、関数本体のthis
が参照するのはそのオブジェクトです。つぎのオブジェクト(deck)のメソッド(createCardPicker())を呼び出すと、52枚のカードから1枚を決めて、そのマークと数字を変数(pickedCard)にオブジェクトで返します。なお、数字は0から12までの整数になります。
let deck = { suits: ['hearts', 'spades', 'clubs', 'diamonds'], createCardPicker: function() { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }; let pickedCard = deck.createCardPicker();
けれど、関数が新たな関数を返した場合、戻り値の関数はまだ呼び出されていません。this
参照は呼び出されるときに決まるので、つぎのコードでは戻り値の関数(cardPicker())を呼び出したグローバル空間が対象となり、もとのオブジェクト(deck)ではなくなります。したがって、オブジェクトのプロパティが見つからないのでエラーになってしまうのです。
let deck = { suits: ['hearts', 'spades', 'clubs', 'diamonds'], createCardPicker: function() { return function() { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; // this参照にプロパティが見つからないのでエラー } } }; let cardPicker = deck.createCardPicker(); let pickedCard = cardPicker();
ECMAScript 2015のアロー関数式で書けば、定められたオブジェクトがthis
で参照されます(「TypeScript入門 09: アロー関数式」参照)。つぎのオブジェクト(deck)のメソッド(createCardPicker())から返された関数(cardPicker())を呼び出すと、52枚のカードから1枚を決めて、マークと数字を変数(pickedCard)にオブジェクトで返します。
let deck = { suits: ['hearts', 'spades', 'clubs', 'diamonds'], createCardPicker: function () { // return function() { return () => { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return { suit: this.suits[pickedSuit], card: pickedCard % 13 }; }; } }; let cardPicker = deck.createCardPicker(); let pickedCard = cardPicker();
ただし、関数を返すオブジェクト(deck)のメソッド(createCardPicker())もアロー関数式にしてしまうと、this
参照が戻り値の関数を変数(cardPicker)に定めたグローバル空間になってしまいます。
let deck = { suits: ['hearts', 'spades', 'clubs', 'diamonds'], createCardPicker: // function () { () => { return () => { return { suit: this.suits[pickedSuit], card: pickedCard % 13 }; // this参照にプロパティが見つからないのでエラー }; } }; let cardPicker = deck.createCardPicker(); // this参照はグローバル空間に定められる let pickedCard = cardPicker();
関数のthis
参照がグローバル空間に定められてしまう問題は、コールバックでも起こります。この場合も、アロー関数式を正しく用いることで避けられるでしょう。
let deck = { suits: ['hearts', 'spades', 'clubs', 'diamonds'], createCardPicker: function() { setTimeout(() => { // function() { // アロー関数式でオブジェクトをthis参照する let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); console.log({suit: this.suits[pickedSuit], card: pickedCard % 13}); }, 1000); } }; deck.createCardPicker();
06 thisを引数で型づける
TypeScriptには、noImplicitThis
のフラグがあります。値をtrue
に定めると、this
は型づけしなければなりません(デフォルト値はfalse
)。
tsconfig.json{ "compilerOptions": { "noImplicitThis": true } }
前項で試したつぎのコードは、this
の型が決まっていないためエラーになります(図002)。
let deck = { suits: ['hearts', 'spades', 'clubs', 'diamonds'], createCardPicker: function () { return () => { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return { suit: this.suits[pickedSuit], card: pickedCard % 13 }; }; } };
図002■thisが型づけされていないというエラー
this
は関数の第1引数で型を与えます。つぎのようにany
で型づけすれば、取りあえずエラーは消えるはずです。
let deck = { suits: ['hearts', 'spades', 'clubs', 'diamonds'], createCardPicker: function (this: any) { // thisを型づけ return () => { return { suit: this.suits[pickedSuit], card: pickedCard % 13 }; // エラーなし }; } };
noImplicitThis
をtrue
にしたからには、インタフェース(interface
)できちんと型を決めましょう。つぎのように定めれば、メソッド(createCardPicker())はインタフェースどおりのオブジェクト(Deck)をthis
参照して呼び出さなければなりません。
interface Card { suit: string; card: number; } interface Deck { suits: string[]; createCardPicker(this: Deck): () => Card; } let deck: Deck = { suits: ['hearts', 'spades', 'clubs', 'diamonds'], createCardPicker: function (this: Deck) { return () => { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return { suit: this.suits[pickedSuit], card: pickedCard % 13 }; }; } };
インタフェースにそぐわないオブジェクトからメソッドを呼び出そうとすれば、this
が型と合わないというエラーを起こします。こうすれば、this
参照の問題は防げるでしょう。
let cardPicker = deck.createCardPicker(); let pickedCard = cardPicker(); console.log(pickedCard); // 選ばれたカードのオブジェクトが返される let test = {cardPicker: deck.createCardPicker}; // pickedCard = test.cardPicker(); // メソッドのthis参照が違うのでプロパティが見つからないというエラー
07 関数を多重定義する
関数に渡す引数や型によって処理を変えることは「多重定義」(オーバーロード)と呼ばれます。引数を省略可能にしたり、any
で型づけすれば処理はできるでしょう。けれど、型を確かめる機能が失われてしまいます[*2]。
let suits = ['hearts', 'spades', 'clubs', 'diamonds']; function pickCard(cards?: any): {suit: string, card: number} { // 引数の数や型が確かめられない let pickedCard: number; let pickedSuit: number; let type = typeof cards; if (type === 'number') { pickedCard = cards; } else if (type === 'undefined') { pickedCard = Math.floor(Math.random() * 52); } else if (type === 'object' && cards.length) { pickedCard = cards[Math.floor(Math.random() * cards.length)]; } else { return null; } pickedSuit = Math.floor(pickedCard / 13); return {suit: suits[pickedSuit], card: pickedCard % 13}; }
TypeScriptでは、関数の実装の前にそれとは分けてfunction
キーワードで型が定められます。実装は引数の型をany
としていても、前に与えられた型づけのいずれにも合わなければエラーになるのです。つぎの関数(pickCard())は、引数に0から51までの整数を渡すとそのカード、引数がなければ52枚中ランダムな1枚、0から51までの整数の配列を渡せばその中の1枚をオブジェクトで返します。
let suits = ['hearts', 'spades', 'clubs', 'diamonds']; function pickCard(cards: number[]): {suit: string, card: number}; function pickCard(cards: number): {suit: string, card: number}; function pickCard(): {suit: string, card: number}; function pickCard(cards?: any): {suit: string, card: number} { let pickedCard: number; let pickedSuit: number; let type = typeof cards; if (type === 'number') { pickedCard = cards; } else if (type === 'undefined') { pickedCard = Math.floor(Math.random() * 52); } else if (type === 'object' && cards.length) { pickedCard = cards[Math.floor(Math.random() * cards.length)]; } else { return null; } pickedSuit = Math.floor(pickedCard / 13); return {suit: suits[pickedSuit], card: pickedCard % 13}; } console.log(pickCard()); // 52枚からのランダムなカード console.log(pickCard([0, 13, 26, 39])); // 4つのAのうちのひとつ console.log(pickCard(0)); // { suit: 'hearts', card: 0 } // pickCard('hearts') // 関数の型づけと合わないのでエラー
関数のオーバーロードでは、引数によって戻り値の型を変えることもできます(「Functions」の「Overloads」に掲げられたサンプルコード参照)。また、クラスのメソッドやコンストラクタもつぎのようにオーバーロードは可能です。
class Vector { public y: number; public x: number; constructor(x: number, y?: number); constructor(coords: {x?: number, y?: number}); constructor(); constructor(x?: any, y?: number) { let type = typeof x; if (type === 'number') { this.x = x; this.y = y ? y : 0; } else if (type === 'object') { this.x = x.x ? x.x : 0; this.y = x.y ? x.y : 0; } else { this.x = 0; this.y = 0; } } scale(x: {x?: number, y?: number}): Vector; scale(x: number, y?: number): Vector; scale(x?: any, y?: number): Vector { let type = typeof x; if (type === 'number') { this.x *= x; this.y *= y ? y : x; } else if (type = 'object') { this.x *= x.x ? x.x : 1; this.y *= x.y ? x.y : 1; } return this; } } console.log(new Vector()); // Vector { x: 0, y: 0 } console.log(new Vector(10)); // Vector { x: 10, y: 0 } console.log(new Vector({x: 1, y: 2})); // Vector { x: 1, y: 2 } console.log(new Vector({y: 3})); // Vector { x: 0, y: 3 } let vector = new Vector(1, 1); console.log(vector.scale(2)); // Vector { x: 2, y: 2 } console.log(vector.scale(2, 1/2)); // Vector { x: 4, y: 1 } console.log(vector.scale({y: 2})); // Vector { x: 4, y: 2 } // vector.scale({z: 2}); // メソッドの型づけと合わないのでエラー
|
と型変換のas
構文を使って、つぎのように書き替えれば型が確かめられます。けれど、オーバーロードの構文を用いたほうが、引数によって処理を変えることがはっきりして、スクリプトも書きやすいでしょう。
let suits = ['hearts', 'spades', 'clubs', 'diamonds']; function pickCard(cards?: number | number[]): {suit: string, card: number} { let pickedCard: number let pickedSuit: number; let type = typeof cards; if (type === 'number') { pickedCard = cards as number; } else if (type === 'undefined') { pickedCard = Math.floor(Math.random() * 52); } else if (type === 'object' && (cards as number[]).length) { pickedCard = cards[Math.floor(Math.random() * (cards as number[]).length)]; } else { return null; } pickedSuit = Math.floor(pickedCard / 13); return {suit: suits[pickedSuit], card: pickedCard % 13}; }
作成者: 野中文雄
作成日: 2017年3月19日
Copyright © 2001-2017 Fumio Nonaka. All rights reserved.