LoginSignup
8
5

More than 5 years have passed since last update.

TypeScript Handbook を読む (5. Functions)

Last updated at Posted at 2017-03-12

TypeScript Handbook を読み進めていく第五回目。

  1. Basic Types
  2. Variable Declarations
  3. Interfaces
  4. Classes
  5. Functions (今ココ)
  6. Generics
  7. Enums
  8. Type Inference
  9. Type Compatibility
  10. Advanced Types
  11. Symbols
  12. Iterators and Generators
  13. Modules
  14. Namespaces
  15. Namespaces and Modules
  16. Module Resolution
  17. Declaration Merging
  18. JSX
  19. Decorators
  20. Mixins
  21. Triple-Slash Directives
  22. Type Checking JavaScript Files

Functions

原文

Functions

TypeScript では JavaScript と同じように名前付き関数と無名関数の両方を使用することができます。

TypeScript
// 名前付き関数
function add(x, y) {
    return x + y;
}

// 無名関数
let myAdd = function(x, y) { return x+y; };

また、JavaScript と同様に関数外の変数を キャプチャ して関数の中から参照することができます。

TypeScript
let z = 100;

function addToZ(x, y) {
    return x + y + z;
}

Function Types

Typing the function

関数の引数や戻り値の型を指定することができます。
この時、TypeScript は return 文から戻り値の型を推測するため、大抵の場合で戻り値の型を省略することができます。

TypeScript
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 つのパートを持っています。
もし関数の完全な型を表現する場合、その両方が必須となります。

TypeScript
let myAdd: (x: number, y: number) => number =
    function(x: number, y: number): number { return x+y; };

(x: number, y: number) => number の部分が関数の型

引数の型を表現する時には、引数リストと同じように引数名と型を指定しますが、ここでの引数名は単に可読性を高めるためだけのものです。
そのため、先ほどの例は以下のようにも記述できます。

TypeScript
let myAdd: (baseValue:number, increment:number) => number =
    function(x: number, y: number): number { return x + y; };

戻り値の型は、引数の型と戻り値の型の間に二重矢印 (=>) を記述することで表現します。
関数の型では戻り値の型も必須なため、戻り値を持たない関数の場合には戻り値の型として void を使用します。

Inferring the types

代入元/先のどちらからで型が省略された場合でも、TypeScript は型推論を行ってくれます。
これを "文脈に基づく型付け" と呼びます。

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 では、デフォルトですべての引数が必須として扱われるため、関数を呼び出す際はすべての引数を指定する必要があります。

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 で実現するには引数名の後ろに ? を付与します。
ただし、任意の引数は必須の引数よりも 後ろ に配置する必要がある点に注意してください。

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 が渡された場合のデフォルト値を指定することもできます。

TypeScript
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"
JavaScript
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 となります。

TypeScript
function buildName(firstName: string, lastName?: string) {
    // ...
}
TypeScript
function buildName(firstName: string, lastName = "Smith") {
    // ...
}

任意の引数と異なり、デフォルト引数は必ずしも必須の引数の後ろに配置する 必要はありません
必須の引数の前の引数を省略する場合、明示的に undefined を指定します。

TypeScript
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 では引数名の前に省略記号 (...) を付与することで、配列としてまとめて引数を受け取ることができます。

TypeScript
function buildName(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
JavaScript
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");

省略記号を関数の型で使用することも可能です。

TypeScript
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 メソッドが返却している関数で使用されている thisdeck ではなく、window を指しているためです。
これは cardPicker を直接呼び出したために起きたことで、このようなトップレベルの非メソッド構文呼び出しでは thiswindow が設定されます。(ただし、strict モードでは window ではなく undefined になります)

トップレベルの非メソッド構文呼び出し とは オブジェクト.メソッド()関数.call/apply() ではない、普通の関数呼び出しのことね

TypeScript
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 のアロー構文を使用することでこの問題を回避できます。

TypeScript
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);
JavaScript
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]thisany 型であると指摘されるでしょう。

ここで言う「誤り」とは、オブジェクトリテラル内で this をキャプチャしたことで、this の型が any になってしまったことを指してるっぽい?
--noImplicitThis フラグは this の型が any の時に警告するものだし。

this parameters

前述の例ではオブジェクトリテラル内で関数式を使用したため、this.suits[pickedSuit] の型が any になってしまっていました。
これを修正するには明示的に this 引数を渡す必要があります。
this 引数はダミーの引数であり、引数リストの最初に指定する必要があります。

TypeScript
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);
JavaScript
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 を使用しないことを明示します。

TypeScript
interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}

こうすると、以下のように誤って this を使用しようとしてもコンパイルエラーになります。

TypeScript
class Handler {
    info: string;
    onClickBad(this: Handler, e: Event) {
        // おっと、ここで this を使おうとしている
        this.info = e.message;
    };
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // エラー!

このエラーを解消するには、以下のように this の型を変更します。

TypeScript
class Handler {
    info: string;
    onClickGood(this: void, e: Event) {
        // this の型が void なので、this を使用することはできない!
        console.log('clicked!');
    }
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);

もしもコールバック関数の中で this を使用したい場合、以下のようにアロー関数を使用する必要があります。

TypeScript
class Handler {
    info: string;
    onClickGood = (e: Event) => { this.info = e.message }
}
JavaScript
var Handler = (function () {
    function Handler() {
        var _this = this;
        this.onClickGood = function (e) { _this.info = e.message; };
    }
    return Handler;
}());

この方法のデメリットは、通常のメソッドは prototype に追加されることでインスタンス間で共有されるのに対し、各インスタンスごとに関数が作成されることです。

つまりメモリが無駄に消費されるというわけ。
まあインスタンスごとにキャプチャしている this が違うわけだから共有は無理よね。

Overloads

JavaScript は非常に動的な言語であり、渡された引数の型に応じて同じ関数から異なる型を返却することは珍しくありません。

TypeScript
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);

この関数で扱っている型を正確に表現するにはどうすれば良いでしょう?
その答えは関数のオーバーロードを使用することです。

TypeScript
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 にオーバーロードの考え方がないため、こうせざるを得ないみたい。

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5