関数
イントロダクション
関数は、JavaScriptでアプリケーションのブロックを構築するのに欠かせない存在です。 抽象化の層、クラスの真似事、情報の隠蔽、モジュール化を組み立てるのに使用されます。
TypeScriptではクラス、名前空間、モジュールが存在しますが、 それでもまだ関数はそれらの事を行う方法を記述するのに重要な役割を果たしています。 また、TypeScriptは標準なJavaScript関数に対して、より使いやすくするための新しい機能を追加します。
関数
TypeScriptの関数はJavaScriptと全く同じように、名前付きの関数、または無名関数として作成することが可能です。 これは、APIの関数リストを作成するのか、または他の関数へ手渡しする1度限りの関数を作成するのか、 アプリケーションにとって最良のアプローチを選択することを可能にしてくれます。
ひとまず、JavaScriptのような見た目の2つのアプローチをお見せするのであれば、次のようになるでしょう。
// 名前付き関数
function add(x, y) {
return x + y;
}
// 匿名関数
let myAdd = function(x, y) { return x+y; };
JavaScriptと同様に、関数は本文の外側の変数を参照することが可能です。 これは、「変数を補足(capture)する」と言われています。 これがどのように動作しているか、このテクニックを使用する際のトレードオフは何なのか、 JavaScriptとTypeScriptを扱う開発者が深く理解しておくべ重要な部分ですが、 それはこの記事の範囲外になります。
let z = 100;
function addToZ(x, y) {
return x + y + z;
}
関数の型
関数への型付け
先ほどの例に型を追加してみましょう。
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x+y; };
各引数に型を追加し、関数自身にも戻り値の型を追加することが出来ます。 TypeScriptでは戻り値の文を見ることで、その型を知ることができ、 また、それを指定するかしないかは任意に決めることができます。
関数の型
ここで、関数の型の各パーツを記述して、完全な関数の型を書いてみましょう。
let myAdd: (x: number, y: number)=>number =
function(x: number, y: number): number { return x+y; };
関数の型は、引数の型と戻り値の型の2つの同じパーツを持ちます。 関数型全体を書く場合、両方のパーツが必須となります。 引数の一覧と同様に、書く引数に名前と型を与えることで、引数の型を書きます。 この名前は読みやすいものにすると良いでしょう。 先ほどの例を、次のように書き換えてみました。
let myAdd: (baseValue:number, increment:number) => number =
function(x: number, y: number): number { return x + y; };
引数の名前に関係なく、列挙される引数の型に沿って、各引数が適切な型かどうかがチェックされます。
2つ目のパーツは、戻り値の型になります。
引数と戻り値の型の愛大に矢印(=>
)を使用することで、戻り値の型を明確にします。
前述したように、関数の型で必須となるパーツなので、
もし関数が値を返さない場合は、何も指定しないのではなく、void
を使用してください。
注目すべきは、引数と戻り値の型だけで関数型を構築していることです。 捕捉される変数は、この型の中には反映されません。 実際には捕捉された変数は関数の"隠されたState"の一部であり、そのAPIを構成するものではありません。
型の推論
サンプルを動かすと、式の片方だけに型を持たせていたとしても、 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; };
これは"文脈上の型付け(contextual typing)"と呼ばれる、型推論の形式です。 これは、プログラムのタイピングを減らすことに大いに貢献してくれることでしょう。
任意引数とデフォルト引数
TypeScriptでは、全ての引数は関数によって必要とされているとみなされます。
これは、null
またはundefined
を渡すことが出来ないという意味ではありませんが、
関数が呼ばれる場合、コンパイラは各引数に提供されている値をチェックします。
また、コンパイラはこれらの引数だけが、関数へ渡される引数であるとみなします。 端的に言えば、関数に渡される引数の数は、関数が期待する引数の数と一致させる必要があります。
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では、この機能を?
を任意にしたい引数の後ろに付けることで実装することが可能です。
例えば、上記の例のlastNameを任意の引数にしてみましょう。
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
任意の引数は、必須の引数の後ろに指定しなければいけません。 もし、lastNameではなくfirstNameを任意にしたいのであれば、 引数の指定の順番を変更する必要があります。
TypeScriptでは、もしユーザーがその引数に値を指定なかった場合、またはundefined
を指定した場合、
その引数に割り当てる値を設定することも可能です。
これは、デフォルト初期化引数(default-initialized parameters)と呼ばれます。
先ほどの例のlastNameに"Smith"をデフォルト値として設定してみましょう。
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // OK、"Bob Smith"を返すようになります
let result2 = buildName("Bob", undefined); // still works, also returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result4 = buildName("Bob", "Adams"); // ah, just right
全ての必須引数の後ろに配置されるデフォルト初期化引数は、任意の引数として扱われるため、 関数の呼び出し時に省略することが可能です。 これは、任意引数と末尾のデフォルト引数はそれらの型が共通することを意味し、そのため、
function buildName(firstName: string, lastName?: string) {
// ...
}
と
function buildName(firstName: string, lastName = "Smith") {
// ...
}
の両方で、共通する(firstName: string, lastName?: string) => string
の型を持つことになります。
lastName
のデフォルト値は、その引数が任意であるという事実だけを残して、型の情報からは消失します。
素の任意の引数とは異なり、デフォルト初期化引数は、必須引数の後ろに配置する必要はありません。
もし、デフォルト初期化引数がいずれかの必須引数よりも前にある場合は、
ユーザーは初期値を取得するには、明示的にundefined
を渡す必要があります。
先ほどの例のfirstName
に、デフォルト初期化引数だけを指定すると次のようになります。
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引数(Rest Parameters)
必須、任意、デフォルトの引数の全てに共通する点は、それらは1つの引数に対して語られることであるということです。 時折、複数の引数をグループとして使用したい、 または最終的に必要な引数の数が事前に分からない、ということがあるかもしれません。 JavaScriptでは、各関数内の本文で引数を可視化することで、それを使用することが可能です。
TypeScriptでは、これらの引数を集めて1つの変数にまとめることが可能です。
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
Rest引数(訳注:Restは「残りの…」という意味)は、任意の引数の数を際限なく取り扱います。
Rest引数として引数が渡されると、望む数だけそれを使用することが可能です。また、何も渡さなくても問題ありません。
コンパイラは、省略記号(...
)の後ろに名前が指定された部分に渡された引数を、
関数内で使用できる配列として構築します。
省略記号は、Rest引数を使用した関数の型にも使用されます。
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
this
JavaScriptでthis
の使い方を学ぶことは、一つの通過儀礼のようなものでしょう。
TypeScriptはJavaScriptのスーパーセットであるため、
TypeScriptの開発者もまた、this
の使い方を学び、それが正しく使われていない箇所を発見できるようになる必要があります。
幸いなことに、TypeScriptは幾つかのテクニックを使用してthis
の誤用を捕らえることを可能にしてくれます。
もし、JavaScriptのthis
の動作について学ぶ必要があるのであれば、
まずUnderstanding JavaScript Function Invocation and "this"を読むことをおすすめします。
Yehuda氏の記事は、this
の内部動作について非常に分かりやすく書かれているため、
this
の基本的なことをここで学ぶことができます。
thisとアロー関数
JavaScriptでは、this
は関数が呼ばれた際に設定される変数です。
これは非常に強力で柔軟な機能ではありますが、
関数が実行されたコンテキストについて知っておかなければいけないというコストが、常に付きまといます。
これは周知のごとく混乱をきたすものであり、特に関数を返す場合、または引数として関数へ渡す場合に、
顕著にそれが現れます。
例を見てみましょう。
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);
createCardPicker
は、それ自身が関数を返す関数であることに注意してください。
もし、この例を実行すると、期待するアラートボックスが表示される代わりに、エラーが発生します。
これは、createCardPicker
によって作成された関数内で使用されるthis
には、
deck
オブジェクトではなく、window
が設定されてしまうためです。
我々がcardPicker()
をそれ自身だけで呼び出していることが原因です。
このような非メソッド構文の最上位層での呼び出しには、this
にwindow
が使用されます。
(注意: strictモード下では、this
はwindow
ではなく、undefined
になります。)
後で使用されることになる関数が返される前に、
それを正しいthis
にバインドすることによって、これを修正することが可能です。
この方法は後でどのように使用されるかに関係なく、
元のdeck
オブジェクトが参照されるようにしてくれます。
これを行うために、ECMAScript 6のアロー文法を使用して関数式を変更してみましょう。
アロー関数は、関数が実行された場所ではなく、関数が作られた場所でthis
を捕捉します。
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);
更に素晴らしいことに、TypeScriptは--noImplicitThis
フラグをコンパイラに渡している場合、
this
の誤用に対して警告を発してくれます。
this.suits[pickedSuit]
のthis
が、any
型であることが指摘されるでしょう。
thisパラメーター
残念ながら、this.suits[pickedSuit]
の型はany
のままです。
これは、this
がオブジェクト・リテラル内部の関数式であるためです。
これを修正するために、明確にthis
の引数を指定します。
this
引数はフェイクであり、これを関数の引数リストの1番目に指定します。
function f(this: void) {
//この関数が独立した状態で、thisを使用できないようにする
}
前述した例にCard
とDeck
のインターフェースを追加し、
型を明確にし、再利用しやすいものにしてみましょう。
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),
// NOTE: The function now explicitly specifies that its callee must be of type 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);
これで、TypeScriptはcreateCardPicker
がDeck
オブジェクト上で呼び出されることを知ることができるようになりました。
これで、this
がany
ではなくDeck
型であることを意味するようになったため、
--noImplicitThis
の指定でエラーが発生しなくなります。
コールバック内のthisパラメーター
後で呼び出すための関数をライブラリに渡し、
そのコールバックでthis
を使用して、エラーが発生してしまうというケースもあります。
渡したコールバックを呼び出すライブラリは、通常の関数のように呼び出されるため、
this
がundefined
になります。
これもある対策をすることで、コールバックでもエラーを発生させずにthis
パラメーターを使用することが可能です。
まず、ライブラリの作者がthis
を含め、コールバックの型に注釈を付ける(annotate)必要があります。
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
this: void
は、addClickListener
が「onclick
がthis
の型を必須としない関数であることを期待する」ことを意味します。
次に、this
を使用する呼び出しのコードに注釈(annotate)を付けます。
class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// ここでthisを使用していまが、thisを使用したコールバックは
// 実行時にクラッシュします。
this.info = e.message;
};
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!
this
に注釈を付けたことで、onClickBad
はHandler
のインスタンスから呼び出されなければいけないことを明確にしました。
TypeScriptはaddClickListener
がthis: void
を持つ関数を必須としているかを検証します。
エラーを修正するために、this
の型を変更します。
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// thisの型がvoidのため、ここではthisを使用できません!
console.log('clicked!');
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);
onClickGood
はthis
の型をvoid
としているため、
addClickListener
へ渡すことは問題ないと判定されます。
もちろん、これもthis.info
を使用することは出来ません。
どちらも両立させたいのであれば、アロー関数を使用する必要があります。
class Handler {
info: string;
onClickGood = (e: Event) => { this.info = e.message }
}
アロー関数はthis
を捕捉しないため、this: void
が期待されるものであれば何でも渡すことができるため、
これは問題なく動作します。
ただし、不都合な点として、Handler
型のオブジェクト毎に、1つのアロー関数が作成されるということが挙げられます。
一方、メソッドは一度だけ作成され、Handler
のprototypeに割り当てられます。
これらはHandler
型のオブジェクト全体で共有されます。
オーバーロード
JavaScriptは非常に動的要素の強い言語です。 ひとつのJavaScript関数が、渡された引数の型(形状)を元に、異なるオブジェクトの型を返すことは珍しいことではありません。
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x): any {
// オブジェクト/配列なのかを判定して、
// もしそうであればデッキ(のインデックス番号)をこちらに渡し、
// それによってカードを引かせます。
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 };
}
}
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);
ここでのpickCard
関数は、ユーザーが渡すものを元にして、2つの異なるものを返します。
もし、ユーザーがデッキを表すオブジェクトを渡すと、関数はその中からカードを選択します。
もし、ユーザーがカード(番号)を選択すれば、どのカードが選択されたかを伝えます。
ただし、この型のシステムをどのように表したらよいのでしょう?
その答えは、オーバーロードのリストとして、同じ関数に複数の関数型を提供するということです。
このリストは、コンパイラによって関数呼び出しの解決に使用されます。
それでは、pickCard
が何を受け取り、何を返すのかを表す、オーバーロードのリストを作成してみましょう。
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 {
// オブジェクト/配列なのかを判定して、
// もしそうであればデッキ(のインデックス番号)をこちらに渡し、
// それによってカードを引かせます。
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 };
}
}
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);
この変更によって、pickCard
関数呼び出しに、オーバーロードによる型チェックが行われるようになりました。
コンパイラがチェックする型を正しく選べるように、基底となるJavaScriptのプロセスと同様のプロセスに従っています。 オーバーロードのリストを確認し、提供された引数を使用して、1つ目のオーバーロードを使用した処理を試みます。 もし、これが適合すれば、正しいオーバーロードであるとして選ばれます。 こうした理由から、最も特徴的な要素が多いものから少ないものの順に並べることが通例となっています。
function pickCard(x): any
は、オーバーロードのリストの一部では無いことに注意してください。
そのため、オーバーロードに含まれるのは、オブジェクト(object)を受け取るものと、数値(number)を受け取るものの2つのみになります。
pickCard
をそれ以外の型のパラメーターで呼び出すと、エラーになります。
© https://github.com/Microsoft/TypeScript-Handbook
このページは、ページトップのリンク先のTypeScript-Handbook内のページを翻訳した内容を基に構成されています。 下記の項目を確認し、必要に応じて公式のドキュメントをご確認ください。 もし、誤訳などの間違いを見つけましたら、 @tomofまで教えていただければ幸いです。
- ドキュメントの情報が古い可能性があります。
- "訳注:"などの断わりを入れた上で、日本人向けの情報やより分かり易くするための追記を行っている事があります。