関数は、JavaScriptにおけるあらゆるアプリケーションの基本的な構成要素です。関数は、クラス、情報隠蔽、モジュールを模倣して、抽象化の層を構築する方法です。TypeScriptでは、クラス、名前空間、モジュールがありますが、関数は依然として、物事を行う方法を記述する上で重要な役割を果たします。また、TypeScriptは、標準のJavaScript関数にいくつかの新しい機能を追加して、より使いやすくしています。
関数
まず、JavaScriptと同様に、TypeScript関数は、名前付き関数としても無名関数としても作成できます。これにより、APIに関数のリストを構築する場合でも、別の関数に渡す一回限りの関数でも、アプリケーションに最適なアプローチを選択できます。
JavaScriptでこれら2つのアプローチがどのように見えるかを簡単にまとめます
tsTry
// Named functionfunctionadd (x ,y ) {returnx +y ;}// Anonymous functionletmyAdd = function (x ,y ) {returnx +y ;};
JavaScriptと同様に、関数は関数本体の外にある変数を参照できます。その場合、これらの変数をキャプチャすると言われます。これがどのように機能するか(およびこの手法を使用する場合のトレードオフ)を理解することは、この記事の範囲外ですが、このメカニズムがどのように機能するかをしっかりと理解することは、JavaScriptとTypeScriptを使用する上で重要な要素です。
tsTry
letz = 100;functionaddToZ (x ,y ) {returnx +y +z ;}
関数型
関数に型を付ける
先ほどの簡単な例に型を追加してみましょう
tsTry
functionadd (x : number,y : number): number {returnx +y ;}letmyAdd = function (x : number,y : number): number {returnx +y ;};
各パラメータに型を追加し、次に関数自体に追加して、戻り値の型を追加できます。TypeScriptは、returnステートメントを見ることで戻り値の型を判断できるため、多くの場合、オプションでこれを省略することもできます。
関数型を記述する
関数に型を付けたので、関数型の各部分を見て、関数の完全な型を記述しましょう。
tsTry
letmyAdd : (x : number,y : number) => number = function (x : number,y : number): number {returnx +y ;};
関数の型には、引数の型と戻り値の型の2つの部分があります。関数全体の型を記述するときは、両方の部分が必要です。パラメータの型は、各パラメータに名前と型を付けて、パラメータリストのように記述します。この名前は、読みやすさを向上させるためのものです。代わりに、次のように記述することもできました
tsTry
letmyAdd : (baseValue : number,increment : number) => number = function (x : number,y : number): number {returnx +y ;};
パラメータの型が一致する限り、関数型でパラメータに指定する名前に関係なく、それは関数の有効な型と見なされます。
2番目の部分は戻り値の型です。パラメーターと戻り値の型の間に矢印 (=>
) を使用して、どちらが戻り値の型であるかを明確にします。前述のように、これは関数型に必須の部分であるため、関数が値を返さない場合は、省略する代わりに void
を使用します。
特筆すべきは、関数型を構成するのはパラメーターと戻り値の型だけであるということです。キャプチャされた変数は型に反映されません。事実上、キャプチャされた変数は任意の関数の「隠れた状態」の一部であり、その API を構成するものではありません。
型の推論
例を試しているうちに、TypeScript コンパイラーが、式の片側にのみ型がある場合でも、型を把握できることに気付くかもしれません。
tsTry
// The parameters 'x' and 'y' have the type numberletmyAdd = function (x : number,y : number): number {returnx +y ;};// myAdd has the full function typeletmyAdd2 : (baseValue : number,increment : number) => number = function (x ,y ) {returnx +y ;};
これは「文脈型付け」と呼ばれ、型推論の一種です。これは、プログラムを型付きにするための労力を削減するのに役立ちます。
オプションパラメーターとデフォルトパラメーター
TypeScript では、すべてのパラメーターは関数で必須であると想定されています。これは、null
または undefined
を指定できないという意味ではなく、むしろ、関数が呼び出されたときに、コンパイラーが、ユーザーが各パラメーターに値を指定したことを確認することを意味します。また、コンパイラーは、これらのパラメーターが関数に渡される唯一のパラメーターであると想定します。簡単に言えば、関数に渡される引数の数は、関数が予期するパラメーターの数と一致する必要があります。
tsTry
functionbuildName (firstName : string,lastName : string) {returnfirstName + " " +lastName ;}letExpected 2 arguments, but got 1.2554Expected 2 arguments, but got 1.result1 =buildName ("Bob"); // error, too few parametersletExpected 2 arguments, but got 3.2554Expected 2 arguments, but got 3.result2 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult3 =buildName ("Bob", "Adams"); // ah, just right
JavaScript では、すべてのパラメーターはオプションであり、ユーザーは適宜省略できます。省略すると、値は undefined
になります。オプションにするパラメーターの末尾に ?
を追加することで、TypeScript でこの機能を利用できます。たとえば、上記の姓パラメーターをオプションにしたいとしましょう。
tsTry
functionbuildName (firstName : string,lastName ?: string) {if (lastName ) returnfirstName + " " +lastName ;else returnfirstName ;}letresult1 =buildName ("Bob"); // works correctly nowletExpected 1-2 arguments, but got 3.2554Expected 1-2 arguments, but got 3.result2 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult3 =buildName ("Bob", "Adams"); // ah, just right
オプションのパラメーターは、必須のパラメーターの後に続く必要があります。姓ではなく名をオプションにする場合は、関数内のパラメーターの順序を変更し、名がリストの最後になるようにする必要があります。
TypeScript では、ユーザーが値を指定しない場合、またはユーザーが undefined
を代わりに渡した場合に、パラメーターに割り当てられる値を設定することもできます。これらはデフォルトで初期化されたパラメーターと呼ばれます。前の例を取り上げて、姓をデフォルトで "Smith"
に設定しましょう。
tsTry
functionbuildName (firstName : string,lastName = "Smith") {returnfirstName + " " +lastName ;}letresult1 =buildName ("Bob"); // works correctly now, returns "Bob Smith"letresult2 =buildName ("Bob",undefined ); // still works, also returns "Bob Smith"letExpected 1-2 arguments, but got 3.2554Expected 1-2 arguments, but got 3.result3 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult4 =buildName ("Bob", "Adams"); // ah, just right
すべての必須パラメーターの後に続くデフォルトで初期化されたパラメーターはオプションとして扱われ、オプションのパラメーターと同様に、それぞれの関数を呼び出すときに省略できます。つまり、オプションのパラメーターと末尾のデフォルトパラメーターは、型に共通性を持つことになり、両方とも
ts
function buildName(firstName: string, lastName?: string) {// ...}
そして
ts
function buildName(firstName: string, lastName = "Smith") {// ...}
同じ型 (firstName: string, lastName?: string) => string
を共有します。lastName
のデフォルト値は型では消え、パラメーターがオプションであるという事実だけが残ります。
単純なオプションのパラメーターとは異なり、デフォルトで初期化されたパラメーターは、必須パラメーターの後に続く必要はありません。デフォルトで初期化されたパラメーターが必須パラメーターの前にある場合、ユーザーはデフォルトで初期化された値を取得するために明示的に undefined
を渡す必要があります。たとえば、firstName
にのみデフォルトの初期化子を使用して、最後の例を記述できます。
tsTry
functionbuildName (firstName = "Will",lastName : string) {returnfirstName + " " +lastName ;}letExpected 2 arguments, but got 1.2554Expected 2 arguments, but got 1.result1 =buildName ("Bob"); // error, too few parametersletExpected 2 arguments, but got 3.2554Expected 2 arguments, but got 3.result2 =buildName ("Bob", "Adams","Sr." ); // error, too many parametersletresult3 =buildName ("Bob", "Adams"); // okay and returns "Bob Adams"letresult4 =buildName (undefined , "Adams"); // okay and returns "Will Adams"
残余パラメーター
必須、オプション、およびデフォルトのパラメーターには、1 つの共通点があります。それは、一度に 1 つのパラメーターについて語るということです。場合によっては、複数のパラメーターをグループとして操作したり、関数が最終的に取得するパラメーターの数を把握していない場合があります。JavaScript では、すべての関数本体内で表示される arguments
変数を使用して、引数を直接操作できます。
TypeScript では、これらの引数を変数にまとめることができます。
tsTry
functionbuildName (firstName : string, ...restOfName : string[]) {returnfirstName + " " +restOfName .join (" ");}// employeeName will be "Joseph Samuel Lucas MacKinzie"letemployeeName =buildName ("Joseph", "Samuel", "Lucas", "MacKinzie");
残余パラメーターは、無制限の数のオプションのパラメーターとして扱われます。残余パラメーターに引数を渡す場合、必要な数だけ使用できます。1 つも渡さないこともできます。コンパイラーは、省略記号 (...
) の後に指定された名前で渡された引数の配列を作成し、関数で使用できるようにします。
省略記号は、残余パラメーターを使用した関数の型でも使用されます。
tsTry
functionbuildName (firstName : string, ...restOfName : string[]) {returnfirstName + " " +restOfName .join (" ");}letbuildNameFun : (fname : string, ...rest : string[]) => string =buildName ;
this
JavaScript で this
を使用する方法を学ぶことは、通過儀礼のようなものです。TypeScript は JavaScript のスーパーセットであるため、TypeScript 開発者も this
の使用方法と、正しく使用されていない場合の見分け方を学ぶ必要があります。幸いなことに、TypeScript では、いくつかのテクニックを使用して this
の誤った使用をキャッチできます。ただし、JavaScript で this
がどのように機能するかを学ぶ必要がある場合は、まず Yehuda Katz の JavaScript の関数呼び出しと「this」の理解をお読みください。Yehuda の記事では this
の内部動作について非常にうまく説明されているため、ここでは基本事項のみを説明します。
this
とアロー関数
JavaScript では、this
は関数が呼び出されたときに設定される変数です。これにより、非常に強力で柔軟な機能になりますが、関数が実行されているコンテキストについて常に知っておく必要があるというコストが発生します。これは、特に関数を返したり、関数を引数として渡したりする場合に、非常に混乱を招く可能性があります。
例を見てみましょう
tsTry
letdeck = {suits : ["hearts", "spades", "clubs", "diamonds"],cards :Array (52),createCardPicker : function () {return function () {letpickedCard =Math .floor (Math .random () * 52);letpickedSuit =Math .floor (pickedCard / 13);return {suit : this.suits [pickedSuit ],card :pickedCard % 13 };};},};letcardPicker =deck .createCardPicker ();letpickedCard =cardPicker ();alert ("card: " +pickedCard .card + " of " +pickedCard .suit );
createCardPicker
は、関数自体が関数を返す関数であることに注意してください。例を実行しようとすると、予期されたアラート ボックスの代わりにエラーが発生します。これは、createCardPicker
によって作成された関数で使用されている this
が、deck
オブジェクトではなく window
に設定されるためです。これは、cardPicker()
を単独で呼び出すためです。このようなトップレベルの非メソッド構文呼び出しでは、this
に window
が使用されます。(注:厳密モードでは、this
は window
ではなく undefined
になります。)
後で使用するために関数を返す前に、関数が正しい this
にバインドされていることを確認することで、これを修正できます。こうすることで、後でどのように使用されても、元の deck
オブジェクトを参照できるようになります。これを行うには、関数式を ECMAScript 6 アロー構文を使用するように変更します。アロー関数は、関数が呼び出された場所ではなく、作成された場所で this
をキャプチャします。
tsTry
letdeck = {suits : ["hearts", "spades", "clubs", "diamonds"],cards :Array (52),createCardPicker : function () {// NOTE: the line below is now an arrow function, allowing us to capture 'this' right herereturn () => {letpickedCard =Math .floor (Math .random () * 52);letpickedSuit =Math .floor (pickedCard / 13);return {suit : this.suits [pickedSuit ],card :pickedCard % 13 };};},};letcardPicker =deck .createCardPicker ();letpickedCard =cardPicker ();alert ("card: " +pickedCard .card + " of " +pickedCard .suit );
さらに、noImplicitThis
フラグをコンパイラーに渡すと、TypeScript はこの間違いを犯したときに警告します。this.suits[pickedSuit]
の this
が any
型であることを指摘します。
this
パラメーター
残念ながら、this.suits[pickedSuit]
の型は依然として any
です。これは、this
がオブジェクト リテラル内の関数式に由来するためです。これを修正するには、明示的な this
パラメーターを指定できます。this
パラメーターは、関数のパラメーター リストの先頭にある偽のパラメーターです。
ts
function f(this: void) {// make sure `this` is unusable in this standalone function}
上記の例に、Card
および Deck
の 2 つのインターフェイスを追加して、型をより明確にし、再利用しやすくしましょう。
tsTry
interfaceCard {suit : string;card : number;}interfaceDeck {suits : string[];cards : number[];createCardPicker (this :Deck ): () =>Card ;}letdeck :Deck = {suits : ["hearts", "spades", "clubs", "diamonds"],cards :Array (52),// NOTE: The function now explicitly specifies that its callee must be of type DeckcreateCardPicker : function (this :Deck ) {return () => {letpickedCard =Math .floor (Math .random () * 52);letpickedSuit =Math .floor (pickedCard / 13);return {suit : this.suits [pickedSuit ],card :pickedCard % 13 };};},};letcardPicker =deck .createCardPicker ();letpickedCard =cardPicker ();alert ("card: " +pickedCard .card + " of " +pickedCard .suit );
これで、TypeScript は createCardPicker
が Deck
オブジェクトで呼び出されることを予期していることを認識します。つまり、this
は any
ではなく、型が Deck
になったため、noImplicitThis
によってエラーが発生することはありません。
コールバックでの this
パラメーター
また、後で呼び出すライブラリに関数を渡す場合、コールバックで this
を使用してエラーが発生する可能性もあります。コールバックを呼び出すライブラリは通常の関数のように呼び出すため、this
は undefined
になります。多少の作業で this
パラメーターを使用して、コールバックでのエラーを防ぐこともできます。まず、ライブラリの作成者は、コールバック型に this
を注釈する必要があります。
tsTry
interfaceUIElement {addClickListener (onclick : (this : void,e :Event ) => void): void;}
this: void
は、addClickListener
が onclick
を this
型を必要としない関数であることを予期していることを意味します。次に、呼び出しコードに this
を注釈します。
tsTry
classHandler {info : string;onClickBad (this :Handler ,e :Event ) {// oops, used `this` here. using this callback would crash at runtimethis.info =e .message ;}}leth = newHandler ();Argument of type '(this: Handler, e: Event) => void' is not assignable to parameter of type '(this: void, e: Event) => void'. The 'this' types of each signature are incompatible. Type 'void' is not assignable to type 'Handler'.2345Argument of type '(this: Handler, e: Event) => void' is not assignable to parameter of type '(this: void, e: Event) => void'. The 'this' types of each signature are incompatible. Type 'void' is not assignable to type 'Handler'.uiElement .addClickListener (h .onClickBad ); // error!
this
を注釈すると、onClickBad
が Handler
のインスタンスで呼び出す必要があることを明示的にします。次に、TypeScript は addClickListener
が this: void
を持つ関数を必要とすることを検出します。エラーを修正するには、this
の型を変更します。
tsTry
classHandler {info : string;onClickGood (this : void,e :Event ) {// can't use `this` here because it's of type void!console .log ("clicked!");}}leth = newHandler ();uiElement .addClickListener (h .onClickGood );
onClickGood
はその this
型を void
として指定しているため、addClickListener
に渡すことは合法です。もちろん、これは this.info
を使用できないことも意味します。両方が必要な場合は、アロー関数を使用する必要があります。
tsTry
classHandler {info : string;onClickGood = (e :Event ) => {this.info =e .message ;};}
これは、アロー関数が外側のthis
を使用するため、this: void
を期待するものに常に渡せるためです。欠点は、Handler型のオブジェクトごとに1つのアロー関数が作成されることです。一方、メソッドは一度だけ作成され、Handlerのプロトタイプにアタッチされます。それらはHandler型のすべてのオブジェクト間で共有されます。
オーバーロード
JavaScriptは本質的に非常に動的な言語です。単一のJavaScript関数が、渡された引数の形状に基づいて異なる型のオブジェクトを返すことは珍しくありません。
tsTry
letsuits = ["hearts", "spades", "clubs", "diamonds"];functionpickCard (x : any): any {// Check to see if we're working with an object/array// if so, they gave us the deck and we'll pick the cardif (typeofx == "object") {letpickedCard =Math .floor (Math .random () *x .length );returnpickedCard ;}// Otherwise just let them pick the cardelse if (typeofx == "number") {letpickedSuit =Math .floor (x / 13);return {suit :suits [pickedSuit ],card :x % 13 };}}letmyDeck = [{suit : "diamonds",card : 2 },{suit : "spades",card : 10 },{suit : "hearts",card : 4 },];letpickedCard1 =myDeck [pickCard (myDeck )];alert ("card: " +pickedCard1 .card + " of " +pickedCard1 .suit );letpickedCard2 =pickCard (15);alert ("card: " +pickedCard2 .card + " of " +pickedCard2 .suit );
ここで、pickCard
関数は、ユーザーが渡した内容に基づいて2つの異なるものを返します。ユーザーがデッキを表すオブジェクトを渡した場合、関数はカードを引きます。ユーザーがカードを選んだ場合、どのカードを選んだかを伝えます。しかし、これを型システムにどのように記述すればよいでしょうか?
その答えは、同じ関数に対して、オーバーロードのリストとして複数の関数型を提供することです。このリストは、コンパイラーが関数呼び出しを解決するために使用します。pickCard
が受け入れるものと返すものを記述するオーバーロードのリストを作成しましょう。
tsTry
letsuits = ["hearts", "spades", "clubs", "diamonds"];functionpickCard (x : {suit : string;card : number }[]): number;functionpickCard (x : number): {suit : string;card : number };functionpickCard (x : any): any {// Check to see if we're working with an object/array// if so, they gave us the deck and we'll pick the cardif (typeofx == "object") {letpickedCard =Math .floor (Math .random () *x .length );returnpickedCard ;}// Otherwise just let them pick the cardelse if (typeofx == "number") {letpickedSuit =Math .floor (x / 13);return {suit :suits [pickedSuit ],card :x % 13 };}}letmyDeck = [{suit : "diamonds",card : 2 },{suit : "spades",card : 10 },{suit : "hearts",card : 4 },];letpickedCard1 =myDeck [pickCard (myDeck )];alert ("card: " +pickedCard1 .card + " of " +pickedCard1 .suit );letpickedCard2 =pickCard (15);alert ("card: " +pickedCard2 .card + " of " +pickedCard2 .suit );
この変更により、オーバーロードはpickCard
関数への型チェックされた呼び出しを提供するようになりました。
コンパイラーが正しい型チェックを選択するために、基盤となるJavaScriptと同様のプロセスに従います。オーバーロードリストを見て、最初から順に、提供されたパラメーターで関数を呼び出そうとします。一致するものが見つかった場合、このオーバーロードを正しいオーバーロードとして選択します。このため、オーバーロードを最も具体的なものから最も具体性の低いものへと順序付けするのが慣例です。
function pickCard(x): any
の部分はオーバーロードリストの一部ではないため、オブジェクトを受け取るものと数値を受け取るものの2つのオーバーロードしかありません。他のパラメーター型でpickCard
を呼び出すとエラーが発生します。