TypeScript Handbook を読み進めていく第五回目。
- Basic Types
- Variable Declarations
- Interfaces
- Classes
- Functions (今ココ)
- Generics
- Enums
- Type Inference
- Type Compatibility
- Advanced Types
- Symbols
- Iterators and Generators
- Modules
- Namespaces
- Namespaces and Modules
- Module Resolution
- Declaration Merging
- JSX
- Decorators
- Mixins
- Triple-Slash Directives
- Type Checking JavaScript Files
Functions
Functions
TypeScript では JavaScript と同じように名前付き関数と無名関数の両方を使用することができます。
// 名前付き関数
function add(x, y) {
return x + y;
}
// 無名関数
let myAdd = function(x, y) { return x+y; };
また、JavaScript と同様に関数外の変数を キャプチャ
して関数の中から参照することができます。
let z = 100;
function addToZ(x, y) {
return x + y + z;
}
Function Types
Typing the function
関数の引数や戻り値の型を指定することができます。
この時、TypeScript は return 文から戻り値の型を推測するため、大抵の場合で戻り値の型を省略することができます。
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x+y; };
Writing the function type
関数の型は引数の型と戻り値の型という 2 つのパートを持っています。
もし関数の完全な型を表現する場合、その両方が必須となります。
let myAdd: (x: number, y: number) => number =
function(x: number, y: number): number { return x+y; };
(x: number, y: number) => number
の部分が関数の型
引数の型を表現する時には、引数リストと同じように引数名と型を指定しますが、ここでの引数名は単に可読性を高めるためだけのものです。
そのため、先ほどの例は以下のようにも記述できます。
let myAdd: (baseValue:number, increment:number) => number =
function(x: number, y: number): number { return x + y; };
戻り値の型は、引数の型と戻り値の型の間に二重矢印 (=>
) を記述することで表現します。
関数の型では戻り値の型も必須なため、戻り値を持たない関数の場合には戻り値の型として void
を使用します。
Inferring the types
代入元/先のどちらからで型が省略された場合でも、TypeScript は型推論を行ってくれます。
これを "文脈に基づく型付け" と呼びます。
// myAdd は関数の型として推論される
let myAdd = function(x: number, y: number): number { return x + y; };
// 引数 'x'、'y' は数値型として推論される
let myAdd: (baseValue:number, increment:number) => number =
function(x, y) { return x + y; };
Optional and Default Parameters
TypeScript では、デフォルトですべての引数が必須として扱われるため、関数を呼び出す際はすべての引数を指定する必要があります。
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // エラー。引数が少ない
let result2 = buildName("Bob", "Adams", "Sr."); // エラー。引数が多い
let result3 = buildName("Bob", "Adams"); // OK
JavaScript ではすべての引数が任意であり、指定しなかった引数は undefined
になっていましたが、これを TypeScript で実現するには引数名の後ろに ?
を付与します。
ただし、任意の引数は必須の引数よりも 後ろ に配置する必要がある点に注意してください。
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
let result1 = buildName("Bob"); // 今回は OK
let result2 = buildName("Bob", "Adams", "Sr."); // エラー。引数が多い
let result3 = buildName("Bob", "Adams"); // OK
引数が指定されなかった、または undefined
が渡された場合のデフォルト値を指定することもできます。
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // 今はOK。戻り値は "Bob Smith"
let result2 = buildName("Bob", undefined); // これも動作する。戻り値は "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // エラー。引数が多い
let result4 = buildName("Bob", "Adams"); // OK。戻り値は "Bob Adams"
function buildName(firstName, lastName) {
if (lastName === void 0) { lastName = "Smith"; }
return firstName + " " + lastName;
}
var result1 = buildName("Bob"); // 今はOK。戻り値は "Bob Smith"
var result2 = buildName("Bob", undefined); // これも動作する。戻り値は "Bob Smith"
var result3 = buildName("Bob", "Adams", "Sr."); // エラー。引数が多い
var result4 = buildName("Bob", "Adams"); // OK。戻り値は "Bob Adams"
必須の引数よりも後ろに配置されたデフォルト引数は、任意の引数として扱われるため、以下の 2 つの関数の型はともに (firstName: string, lastName?: string) => string
となります。
function buildName(firstName: string, lastName?: string) {
// ...
}
function buildName(firstName: string, lastName = "Smith") {
// ...
}
任意の引数と異なり、デフォルト引数は必ずしも必須の引数の後ろに配置する 必要はありません。
必須の引数の前の引数を省略する場合、明示的に undefined
を指定します。
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // エラー。引数が少ない
let result2 = buildName("Bob", "Adams", "Sr."); // エラー。引数が多い
let result3 = buildName("Bob", "Adams"); // OK。戻り値は "Bob Adams"
let result4 = buildName(undefined, "Adams"); // OK。戻り値は "Will Adams"
Rest Parameters
複数の引数をまとめて受け取りたい場合や引数の数が不定の場合には、JavaScript では arguments
変数を直接使用していましたが、TypeScript では引数名の前に省略記号 (...
) を付与することで、配列としてまとめて引数を受け取ることができます。
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
function buildName(firstName) {
var restOfName = [];
for (var _i = 1; _i < arguments.length; _i++) {
restOfName[_i - 1] = arguments[_i];
}
return firstName + " " + restOfName.join(" ");
}
var employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
省略記号を関数の型で使用することも可能です。
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
this
this
and arrow functions
JavaScript における this
は非常に便利で柔軟な一方で、正しく使うには常に関数の実行コンテキストを意識しておく必要があります。
例えば、以下の例を実行するとエラーになります。
なぜなら、createCardPicker
メソッドが返却している関数で使用されている this
は deck
ではなく、window
を指しているためです。
これは cardPicker
を直接呼び出したために起きたことで、このようなトップレベルの非メソッド構文呼び出しでは this
に window
が設定されます。(ただし、strict モードでは window
ではなく undefined
になります)
トップレベルの非メソッド構文呼び出し とは
オブジェクト.メソッド()
や関数.call/apply()
ではない、普通の関数呼び出しのことね
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
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};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
ECMAScript 6 のアロー構文を使用することでこの問題を回避できます。
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
// アロー関数を用いることで 'this' をキャプチャする
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();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
var deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function () {
var _this = this;
// アロー関数を用いることで 'this' をキャプチャする
return function () {
var pickedCard = Math.floor(Math.random() * 52);
var pickedSuit = Math.floor(pickedCard / 13);
return { suit: _this.suits[pickedSuit], card: pickedCard % 13 };
};
}
};
var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
さらに、コンパイラへ --noImplicitThis
フラグを設定することで、このような誤りを警告させることができます。
この例では、this.suits[pickedSuit]
の this
が any
型であると指摘されるでしょう。
ここで言う「誤り」とは、オブジェクトリテラル内で
this
をキャプチャしたことで、this
の型がany
になってしまったことを指してるっぽい?
--noImplicitThis
フラグはthis
の型がany
の時に警告するものだし。
this
parameters
前述の例ではオブジェクトリテラル内で関数式を使用したため、this.suits[pickedSuit]
の型が any
になってしまっていました。
これを修正するには明示的に this
引数を渡す必要があります。
this
引数はダミーの引数であり、引数リストの最初に指定する必要があります。
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// この関数の呼び出し先が Deck であることを明示的に指定する
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};
}
}
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
var deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// この関数の呼び出し先が Deck であることを明示的に指定する
createCardPicker: function () {
var _this = this;
return function () {
var pickedCard = Math.floor(Math.random() * 52);
var pickedSuit = Math.floor(pickedCard / 13);
return { suit: _this.suits[pickedSuit], card: pickedCard % 13 };
};
}
};
var cardPicker = deck.createCardPicker();
var pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
this
引数は JavaScript コードからは削除されるのね。
とは言え、毎回this
引数を指定するのも大変なので、--noImplicitThis
フラグを指定しつつ、警告が出たらthis
引数を指定するくらいが落とし所かな。
this
parameters in callbacks
コールバック関数を指定するようなライブラリにおいても、this
に関するエラーに巻き込まれるでしょう。
これを回避するためには、まず以下のように this: void
と宣言することで、コールバック関数で this
を使用しないことを明示します。
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
こうすると、以下のように誤って this
を使用しようとしてもコンパイルエラーになります。
class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// おっと、ここで this を使おうとしている
this.info = e.message;
};
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // エラー!
このエラーを解消するには、以下のように this
の型を変更します。
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// this の型が void なので、this を使用することはできない!
console.log('clicked!');
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);
もしもコールバック関数の中で this
を使用したい場合、以下のようにアロー関数を使用する必要があります。
class Handler {
info: string;
onClickGood = (e: Event) => { this.info = e.message }
}
var Handler = (function () {
function Handler() {
var _this = this;
this.onClickGood = function (e) { _this.info = e.message; };
}
return Handler;
}());
この方法のデメリットは、通常のメソッドは prototype に追加されることでインスタンス間で共有されるのに対し、各インスタンスごとに関数が作成されることです。
つまりメモリが無駄に消費されるというわけ。
まあインスタンスごとにキャプチャしているthis
が違うわけだから共有は無理よね。
Overloads
JavaScript は非常に動的な言語であり、渡された引数の型に応じて同じ関数から異なる型を返却することは珍しくありません。
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x): any {
// 引数が object または array の場合、
// デッキが渡されたとみなしてその中からカードを引く
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard; // 戻り値はデッキ内のインデックス (number)
}
// それ以外の場合、指定されたカードを引く
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 }; // 戻り値は引いたカード (object)
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
この関数で扱っている型を正確に表現するにはどうすれば良いでしょう?
その答えは関数のオーバーロードを使用することです。
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// 引数が object または array の場合、
// デッキが渡されたとみなしてその中からカードを引く
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard; // 戻り値はデッキ内のインデックス
}
// それ以外の場合、指定されたカードを引く
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 }; // 戻り値は引いたカード (object)
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
オーバーロードは宣言された順番に、指定された引数にマッチするものがないか検索されるため、より限定的なものから順に宣言することが一般的です。
function pickCard(x): any
はオーバーロードの中に含まれないため、オブジェクトと数値以外の引数で pickCard
関数を呼びだそうとするとエラーになります。
いまいち明確にオーバーロードの記法が説明されていないけど、処理を含まない関数宣言だけを並べて、その後に処理を含む同名の関数を定義したら OK かな?
また、実際に処理を行う関数については型をany
にしなくても、オーバーロード宣言と互換性のある型なら良いようだ。
となると、IDE の補完のことを考えて共用型とかにした方が良い気がする。
オーバーロードがある他の言語に慣れていると引数が
any
のメソッドの中で型を基に分岐させなくても、それぞれの引数に応じた関数として実装すれば良いんでは? と思うかもしれないけど、元々 JavaScript にオーバーロードの考え方がないため、こうせざるを得ないみたい。