関数 (Functions)

JavaScriptでは関数は第一級オブジェクトです。よって変数に代入したりすることも可能です。また筆記方法も複数存在し、TypeScriptもそれを継承しています。

関数の宣言

関数の宣言は主に3通りの方法があります。以下は全て同じ関数increment()を宣言しています。

名前付き関数(Normal Functions)

function increment(num: number): number {
return num + 1;
}

匿名関数(Anonymous Functions)

const increment = function(num: number): number {
return num + 1;
};

匿名かつアロー関数(Arrow Functions)

const increment = (num: number): number => {
return num + 1;
};

本書では関数は全て名前付き関数での記述となっています。

また、匿名かつアロー関数は、1行で戻り値を返却できる場合はさらに短縮することができます。

匿名かつアロー関数1行版

const increment = (num: number): number => num + 1;

この時はreturn書いてはいけないので注意してください。1行と書きましたが、厳密にはステートメントがひとつであれば改行しても問題ありません。

さらに、引数が1個である場合は()も省略できます。

匿名かつアロー関数1行かつ引数が1個版

const increment = num => num + 1;

ただし、これができるのは引数が1個の時のみで、0個の時や複数ある時はできません。 また、この時は引数と戻り値に対して型をつけることができません

アロー関数の1行版でオブジェクトリテラルを返したい時はそのまま返すことができません。

const func = () => {x: 1};
console.log(func());
// -> undefined

この時はオブジェクトリテラルを()で括ってください。

const func = () => ({x: 1});
console.log(func());
// -> { x: 1 }

今では使う機会も減りましたがgeneratorという特殊な関数を作ることもできます。ですがこの関数はアロー関数での表記を認めておらず、必ずfunction*() {}と書く必要があります。

this

アロー関数は他と異なる点がいくつかあります。特に注意しなければならないのはthisです。例えば以下のようなクラスJetLagを考えます。

class JetLag {
private message: string;
public constructor(message: string) {
this.message = message;
}
public replyFunction(ms: number): void {
setTimeout(function() {
console.log(this.message);
}, ms);
}
public replyArrow(ms: number): void {
setTimeout(() => {
console.log(this.message);
}, ms);
}
}

このクラスのメソッドreplyFunction(), replyArrow()は、どちらも指定したミリ秒後にコンストラクタで指定した文字列を表示するように見えますがreplyFunction()に問題があります。

const jetlag: JetLag = new JetLag('i can hear you later');
jetlag.replyFunction(10);
// 'this' implicitly has type 'any' because it does not have a type annotation.
// An outer value of 'this' is shadowed by this container.
jetlag.replyArrow(10);
// -> 'i can hear you later'

これはアロー関数と匿名関数でthisが意味するコンテキストが違うために起こります。アロー関数は宣言時にthisであるものを使用するのに対してそれ以外は実行時にthisであるものを使用します。

不定なthisをはっきりさせる

匿名関数を使う時にthisを確定させる方法として以下のふたつがあります。以下は上記クラスのreplyFunction()を書き換えていると解釈してください。

使いたいthisを退避させる

一度thisをほかの変数に代入してあとで呼び出します。

public replyFunction(ms: number): void {
const self: this = this;
setTimeout(function() {
console.log(self.message);
}, ms);
}

匿名関数thisを束縛する

functionにはbind()という関数があります。その関数にfunctionの中でthisとして使用したい変数を引数に入れます。

public replyFunction(ms: number): void {
setTimeout(function() {
console.log(this.message);
}.bind(this), ms);
}

ただし、この方法だけではJavaScriptでは実行できるのですが、TypeScriptでは実行できません。このbind()を使って意図する動作を得るには後述する引数のthisを併せて使う必要があります。

アロー関数が実装される前まではほぼ必須だったthisの取り扱いですが、現在ではそこまで必要ではなくなりました。

関数の型

匿名関数、匿名かつアロー関数では変数に代入していることからわかるように、関数も型による表現が可能です。

ページの初めに登場した関数increment()では関数の型はこのようになります。

(num: number) => number;

これは匿名かつアロー関数と少々宣言が異なります。厳密には戻り値の位置が異なります。匿名かつアロー関数は、実体を除けば以下の形をしています。

(num: number): number => {...};

オブジェクト風の書き方もあります。

type Operate = {
(num: number): number;
};

この書き方はオーバーロードで目にする機会があるかと思います。オーバーロードについては後述しますのでこのまま読み進めてください。

引数(Arguments)

関数の入力値である引数は特殊なことをしない限り、要求する型の変数を、要求する数だけ入力しなければいけません。 例えば原点との距離を求める以下の関数があったとします。

function distance(p: Point): number {
return (p.x ** 2 + p.y ** 2) ** (1 / 2);
}

なお、xy座標上の点を表すPointの定義は以下です。

type Point = {
x: number;
y: number;
};

関数distance()は平面状にある点(x, y)の原点からの距離を返します。この関数を呼ぶ時は必ず引数の数、順番は揃わなければなりません。つまり以下のような関数呼び出しはできません。

引数が少ない

distance();
// Expected 1 arguments, but got 0.

引数が多い

distance(q1, q2);
// Expected 1 arguments, but got 2.

JavaScriptでは引数が少ない時はその引数にはundefinedが渡され、引数が多い場合は余分な引数は無視されるのですが、ここはTypeScriptとJavaScriptとの大きな違いです。

引数を省略したい

引数を省略したいことがあります。その時はオプション引数とデフォルト引数を使用することができます。

上記の関数distance()は、現在は与えられた座標を元に原点からの距離を計算していますが、これを2点の距離を計算できるようにしたいとします。すると上記の関数distance()は以下のようになります。

function distance(p1: Point, p2: Point): number {
return ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** (1 / 2);
}

オプション引数(Optional Parameters)

ここで、第2引数は省略可能にし、省略した場合は第1引数と原点の距離を返したいとします。これはオプション引数を使用すると以下のように書けます。

function distance(p1: Point, p2?: Point): number {
return ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** (1 / 2);
}
distance(q1, q2);
distance(q1);

引数のp2の右隣に?がつきました。これでp2は省略可能な引数となり5, 6行目のどちらの書き方も受け付けるようになります。

しかし、このオプション引数は意味する型が少々変わります。内部的にはp2PointではなくPoint | undefinedのユニオン型として解釈されます。ユニオン型の説明は先の章にあるため詳しい説明は譲りますが、ユニオン型は日本語で言うとどれかの意味です。

ユニオン型が与えられた時は、どちらの型にもあるプロパティ、メソッドでなければ使うことができません。当然ながらundefinedにはx, yというプロパティは存在しないため、上記のコードはTypeScriptに指摘されます。

この問題を解消したのが以下のふたつです。

省略時の初期化処理を書く

function distance(p1: Point, p2?: Point): number {
let p0: Point | undefined = p2;
if (p0 === undefined) {
p0 = {
x: 0,
y: 0
};
}
return ((p1.x - p0.x) ** 2 + (p1.y - p0.y) ** 2) ** (1 / 2);
}

省略時はどの値を使うかという処理が明文化されますが、後述のデフォルト引数がほぼ同じことをできます。これで実装できる場合はデフォルト引数の使用を検討してください。

処理を分ける

function distance(p1: Point, p2?: Point): number {
if (p2 === undefined) {
return (p1.x ** 2 + p1.y ** 2) ** (1 / 2);
}
return ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** (1 / 2);
}

ifp2undefinedの時に実行され、内部でreturnをしています。そのためifブロックの下はp2Pointであることが確定します。そのためTypeScriptはユニオン型から普通のPointとして解釈し、このように書くことができるようになります。

Point | undefined との違い

p2の型がPoint | undefinedとして解釈されるのなら、あえて?などという記号を新しく定義する必要などないのではと思われるかもしれませんが、明確な違いがあります。それは呼び出し側で省略できるかどうかということです。上記の通りオプション引数は省略が可能なのですが、undefinedとのユニオン型であることを明記すると省略ができません。

function distance(p1: Point, p2: Point | undefined): number
// ...
}
distance(q1, q2);
distance(q1);
// Expected 2 arguments, but got 1.
distance(q1, undefined);

6行目のような書き方は指摘を受けます、動作させるためには9行目のように書かなければいけません。

省略可能なundefinedをTypeScriptはどう解釈しているか

実はこのオプション引数としても使われる省略可能なundefinedは、TypeScriptはvoidという専用の型を作って定義しています。つまりオプション引数は以下のように書き換えることもできます。

function distance(p1: Point, p2: Point | void): number
// ...
}
distance(q1, q2);
distance(q1);

このvoid型は値を指定しないundefinedと、意図的に指定しているundefinedの2値を持っています。undefined型は意図的に指定しているundefinedの1値のみを持っているため、このような差が生まれます。

オプション引数でできないこと

オプション引数は必ず最後に書かなければいけません。つまり、以下のようにオプション引数より後ろに普通の引数を書くことはできません。

function distance(p1?: Point, p2: Point): number {
// ...
}
// A required parameter cannot follow an optional parameter.

デフォルト引数(Default Parameters)

省略した時、原点との距離を求めるといったわかりやすい例であればいいのですが(1, 2)との距離を求める、といった変化球がきたとします。なにも考えないとこのようになります。

function distance(p1: Point, p2?: Point): number {
if (p2 === undefined) {
return ((p1.x - 1) ** 2 + (p1.y - 2) ** 2) ** (1 / 2);
}
return ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** (1 / 2);
}

もちろん動くのですが、意図がわかりにくくなってしまいます。このような時に便利なのがデフォルト引数です。デフォルト引数を使用すると以下のように書けます。

const p0: Point = {
x: 1,
y: 2
};
function distance(p1: Point, p2: Point = p0): number {
return ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** (1 / 2);
}
distance(q1, q2);
distance(q1);
distance(q1, undefined);

入力がなかった時に初期値として使いたい値を、その引数の右に書きます。ここではp2の右側の= p0がそれにあたります。

オプション引数と違いユニオン型ではないため、処理の分岐が不要になります。拡張性や見通しを考えればデフォルト引数の方に軍配が上がるでしょう。

初期値に関数の戻り値を使う

デフォルト引数には関数の戻り値を指定することができます。例えば、ある(x, y)が与えられると転置した(y, x)を返すinverse()という関数の戻り値を初期値として使用します。ちなみにinverse()は以下です。

function inverse(p: Point): Point {
return {
x: p.y,
y: p.x
};
}

これを使うとdistance()は以下のようになります。

function distance(p1: Point, p2: Point = inverse(p1)): number {
return ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** (1 / 2);
}

また、デフォルト引数はオプション引数と異なりその引数を最後に書く必要はありません。呼び出し側でデフォルト引数を使用させたい時はundefinedを指定します。この時nullではこの役目を果たせないので注意してください。もちろん末尾のデフォルト引数であれば省略が可能です。

const p0: Point = {
x: 1,
y: 2
};
function distance(p1: Point = p0, p2: Point): number {
return ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** (1 / 2);
}
distance(q1, q2);
distance(undefined, q2);
distance(null, q2);
// Argument of type 'null' is not assignable to parameter of type 'Point | undefined'.

デフォルト引数でできないこと

しかしながら、関数をデフォルト引数として使う時は非同期の関数を使うことができません。TypeScriptならびにJavaScriptは処理を非同期的に扱うことが多く、Promise / async / awaitといった非同期処理を同期的に扱うための機能があります。詳細は先の章にあるため詳しい説明は譲りますが非同期関数が値を返すまで処理を待つということはできません。

async function distanceAync(p1: Point, p2: Point = await inverseAync(p1)): Promise<number> {
return ((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2) ** (1 / 2);
}

このようにデフォルト引数を書くことはできません。なおinverseAsync()は非同期関数とします。

残余引数(Rest Parameters)

いわゆる可変の引数のことです。たとえば引数に与えられた数値の平均を返す関数average()を作るとします。これを残余引数を使って表現すると以下のようになります。

function average(...nums: number[]): number {
if (nums.length === 0) {
return 0;
}
return nums.reduce((prev: number, cur: number): number => {
return prev + cur;
}) / nums.length;
}
console.log(average()); // 0
console.log(average(1, 3)); // 2

0除算を防ぐ目的で少々処理が複雑になっていますが、可変の引数の前に...を付ければ、可変引数を受け付け、それらを配列として受けることができます。

残余引数でできないこと

残余引数は最終的に配列として解釈されるからといって、引数をまとめてひとつの配列として渡すことはできません。

average([1, 3, 5, 7, 9]);

このように配列を直接渡してしまうとaverage()の関数内では要素数1のnumber[][]型が渡されたと解釈されます。もちろんaverage()の期待する引数の型はnumber[]型なので、このコードを実行することはできません。

また、可変個の引数を受け付ける関係上、残余引数より後ろにほかの引数を置くことができません。

function average(...nums: number[], subject: string): number {
// ...
}
// A rest parameter must be last in a parameter list.

ただし残余引数の前であれば問題ありません。

function average(subject: string, ...nums: number[]): number {
// ...
}

スプレッド構文(Spread Syntax)

JavaScriptに組み込みのメソッドとして存在するMath.max()は与えられたnumber型の引数の最大値を返却しますが引数として残余引数を要求します。上記の通り配列をそのまま入れることができないので以下のようなことができません。

const scores: number[] = mathExamination();
const max: number = Math.max(scores);

この例は学校の数学の試験をイメージしています。生徒の数は1年間ではそう増減はしないので、40人ぐらいの生徒なら力技でもなんとかなるかもしれません。

Math.max(scores[0], scores[1], scores[2], scores[3], scores[4], scores[5], scores[6], ...);

書いている最中で力つきました。しかもこれは生徒が転入したり転校したりするとコードを書き換えなければならなくなります。

このような時はスプレッド構文を使って配列を引数の列に変換します。

const scores: number[] = mathExamination();
const max: number = Math.max(...scores);

残余引数もスプレッド構文もどちらも...と表記しますが片方は個々の引数を配列にし、もう片方は配列を個々の引数にします。

分割代入(Destructuring Assignment)

例えばBMI(Body Mass Index)を計算したいとします。身長(cm)と体重(kg)が与えられれば関数bmi()は以下のような計算になります。

function bmi(height: number, weight: number): number {
const mHeight: number = height / 100.0;
return weight / (mHeight ** 2);
}

この関数は引数がどちらもnumber型なので入れ替えてしまうことがあります。22は平均的な体型ですが402はややとてつもなく肥満だと言えます。

bmi(170, 65);
// -> 22.49134948096886
bmi(65, 170);
// -> 402.36686390532543

このような誤用を避けるための方法として分割代入を使うことができます。分割代入を使うと以下のように書きなおせます。

type TopSecret = {
height: number;
weight: number;
};
function bmi({height, weight}: TopSecret): number {
const mHeight: number = height / 100.0;
return weight / (mHeight ** 2);
}

呼び出しは以下のようになります。これならheightweightの意味を取り違えない限り問題は起こりにくくなるでしょう。以下は同じ結果を返します。

bmi({height: 170, weight: 65});
bmi({weight: 65, height: 170});

すでにheight, weightという変数が定義済みであればこのように書くこともできます。

const height: number = 170;
const weight: number = 65;
bmi({height, weight});
bmi({weight, height});

分割代入でうれしいこと

分割代入は普通の引数と異なり以下のような利点があります。

引数の順番にとらわれない

これは、上記の通りです。

デフォルト引数と併用できる

身長あるいは体重を省略できるようにして、省略時に初期値を入れるようにすることができます。

function bmi({height = 165, weight = 60}: Partial<TopSecret>): number {
const mHeight: number = height / 100.0;
return weight / (mHeight ** 2);
}

なお、Partial<T>とは、オブジェクトTのプロパティ、メソッドを省略可能にします。つまりPartial<TopSecret>は以下と同じです。この時の?は引数で説明したオプション引数と意味するものは同じです。

このPartial<T>については専門に解説しているページがありますので併せて参照ください。

type PartialTopSecret = {
height?: number;
weight?: number;
};

これによって呼び出し側はbmi()を以下のどのような方法でも呼び出すことができます。

bmi({});
bmi({height: 180});
bmi({weight: 75});
bmi({height: 180, weight: 75});
bmi({weight: 75, height: 180});

さらに以下のように引数の型の右にもデフォルト引数を付けてあげれば引数自体を省略することができるようになります。

function bmi({height = 165, weight = 60}: Partial<TopSecret> = {}): number {
// ...
}
bmi();

引数のthis

アロー関数以外の関数とクラスのメソッドの第1引数はthisという特殊な引数を受けることができます。これは使用するコンテキストによってthisの意味するところが変わってしまうこれらがどのコンテキストで使用されるべきなのかをTypeScriptに伝えるために使います。このthisは呼び出す側は意識する必要はありません。第2引数以降を指定してください。

アロー関数はこのthisを持つことができません。それは前述のとおりアロー関数は宣言時のthisが使われるためthisが変動することがなく、そもそも不要だからです。

class Male {
private name: string;
public constructor(name: string) {
this.name = name;
}
public toString(): string {
return `Monsieur ${this.name}`;
}
}
class Female {
private name: string;
public constructor(name: string) {
this.name = name;
}
public toString(this: Female): string {
return `Madame ${this.name}`;
}
}

上記クラスMale, Femaleはほぼ同じ構造ですがtoString()のメソッドが異なります。

Male, Femaleはともに普通の用途で使うことができます。

const male: Male = new Male('Frédéric');
const female: Female = new Female('Frédérique');
male.toString();
// -> 'Monsieur Frédéric'
female.toString();
// -> 'Madame Frédérique'

ですが、各インスタンスのtoStringを変数に代入すると意味が変わります。

const maleToStr: () => string = male.toString;
const femaleToStr: (this: Female) => string = female.toString;
maleToStr();
femaleToStr();
// The 'this' context of type 'void' is not assignable to method's 'this' of type 'Female'.

femaleToStr()のコンテキストがFemaleではないとの指摘です。このコードを実行することはできません。 ちなみにこの対応をしていないmaleToStr()は実行こそできますが実行時に例外が発生します。

return `Monsieur ${this.name}`;
^
TypeError: Cannot read property 'name' of undefined

引数のthisを指定することによって意図しないメソッドの持ち出しを避けることもできます。

戻り値 (Return Value)

関数の戻り値を指定することができます。あえて書かなくてもTypeScript側で補完し、関数の戻り値として提供してくれますが、意図しない戻り値を返していないかの検査が働くので書いた方が良いでしょう。前述の通り書く位置が実装と型で異なることに注意してください。

戻り値はJavaScriptと同じく1値のみの返却です。一度に多くの値を戻したい場合はタプルの章を参照してください。

戻り値がないことを明示したい時はvoidと書きます。内部ではundefinedを返していることと同義です。undefinedvoidの違いについては次項で説明します。

void

ここで取り上げるvoidは型のvoidです。JavaScriptにある演算結果を全てundefinedとして返却する演算子ではありません。

主に戻り値で使われる型です。戻り値がvoid型であるとはその関数は戻り値を持っていないことを意味します。

JavaScriptにはこの型は存在しません。void型の実際の値はundefinedです。なぜTypeScriptはundefinedではなく、あえてvoidを用意したのでしょうか。

変数の型として使う

変数の型としてvoidを使うことはほぼありませんが、使うことがあればvoidは値undefinedを代入する変数として使用できます。

function returnUnfefined(): undefined {
return undefined;
}
function returnVoid(): void {
}
const u1: undefined = returnUnfefined();
const u2: void = returnUnfefined();
const v1: undefined = returnVoid();
// Type 'void' is not assignable to type 'undefined'.
const v2: void = returnVoid();
const v1: void = undefined;
const u1: undefined = undefined;

代入時は異なる挙動になります。undefined型の変数をvoid型の変数に代入することができる一方で、void型の変数をundefined型の変数に代入することはできません。

const v2: void = u1;
const u2: undefined = v1;
// Type 'void' is not assignable to type 'undefined'.

関数の引数として使う

変数同様、意図的に引数をvoid型にすることはほぼありませんが、後に登場するジェネリクスで必要になることがあります。

何もしない関数を作ります。

function doNothing1(arg: undefined): any {
// NOOP
}
function doNothing2(arg: void): any {
// NOOP
}

これらは引数の型が違うだけです。これらに先ほど定義したvoid型の変数とundefined型の変数を代入します。すると変数の型について説明したようにundefined型を要求する関数doNothing1()void型の変数を代入することはできません。

doNothing1(u1);
doNothing1(v1);
// Argument of type 'void' is not assignable to parameter of type 'undefined'.
doNothing2(u1);
doNothing2(v1);

これだけではありません。void型を引数に指定した関数doNothing2()は引数を省略することができるようになります。

doNothing1();
// Expected 1 arguments, but got 0.
doNothing2();

この引数のvoid型の挙動は引数の省略の時に登場した?に似ています。

function distance1(p1: Point, p2?: Point): number {
// ...
}
function distance2(p1: Point, p2: Point | void): number {
// ...
}

関数の戻り値として使う

voidの用途はほぼこれです。戻り値の型をvoid型にするとreturnを書かなくてもよい関数を作ることができます。今度は戻り値にvoidundefinedを設定した何もしない関数を作ります。

function doNothing1(): undefined {
//
};
function doNothing2(): void {
//
};
doNothing1();
doNothing2();

するとundefinedを戻り値に指定した関数doNothing1()に以下のような指摘が現れます。

A function whose declared type is neither 'void' nor 'any' must return a value.

これは戻り値がvoid型でもany型でもない関数はreturnを省略できないことを意味しています。この指摘を回避するためにはdoNothing1()は明示的にundefinedを返すか値を書かないreturnが必要です。

以下のどちらかであればdoNothing1()はTypeScriptから指摘を受けません。

function doNothing1(): undefined {
return;
};
function doNothing1(): undefined {
return undefined;
};

voidとは

これらを考慮するとundefined型はundefinedという1値だけを持つ型なのに対しvoid型はそれに加えて何も書かなかった時に代入される非明示のundefinedの2値を持っている型です。

Type Predicate

プログラムを書いているとその変数が意図する型なのかをはっきりさせたい時があります。

function isDuck(animal: Animal): boolean {
if (walksLikeDuck(animal)) {
if (quacksLikeDuck(animal)) {
return true;
}
}
return false;
}

動物がアヒルかどうかを判定するその名もisDuck()です。

ですが、この関数は使用者に対してその変数がアヒルかどうかを伝えているだけです。TypeScriptに対してそれがアヒルであることを伝えるためには型アサーションが必要になります。

if (isDuck(animal)) {
const duck: Duck = animal as Duck;
duck.quacks();
// ...
}

asはTypeScriptにおけるキャストの一種です。名前を型アサーションと言いますが他の言語と異なりかなり強引な型変換ができてしまいますので使用には気をつけてください。ちなみに別の表記方法があるのですが現在はあまり推奨されていません。理由はJSXのタグと見分けがつかないからです。

const duck1: Duck = animal as Duck;
const duck2: Duck = <Duck> animal;

これをより賢くやろうというのがtype predicateです。

type predicateの宣言

type predicateの宣言は戻り値がboolean型の関数に対して適用でき、戻り値のbooleanを以下のように書き替えます。

function isDuck(animal: Animal): animal is Duck {
// ...
}

これで関数isDuck()trueを返す時のifのブロックの中ではanimalDuck型として解釈されるようになります。

if (isDuck(animal)) {
animal.quacks();
// ...
}

しかしながら、これはあくまでもその型であるとTypeScriptに解釈させるだけなので、その型であることがJavaScriptとして正しいということは断言できません。

function isUndefined(value: unknown): value is undefined {
return typeof value === 'number';
}

上記関数isUndefined()は明らかに誤っていますが、この誤りに対してTypeScriptは何も警告を出しません。

Assertion Functions

やりたいことはほぼtype predicateと同じです。type predicateはboolean型の戻り値に対して使用しましたがこちらは例外を投げるかどうかで判定します。上記関数isDuck()をassertion functionsで書きかえると以下のようになります。

function isDuck(animal: Animal): asserts animal is Duck {
if (walksLikeDuck(animal)) {
if (quacksLikeDuck(animal)) {
return;
}
}
throw new Error('YOU ARE A FROG!!!');
}
// ...
isDuck(animal);
animal.quacks();

こちらはこの関数が呼ばれた後であればいつでも変数animalDuck型として解釈されます。

type predicate, assertion functionsのつかいかた

値が存在するかしないかを表現する時、言語によってはOptionalという入れ物のクラスを用意することがあります。このクラスを抽象クラスとして定義し、サブクラスに値が存在するSomeと存在しないNoneを用意するとOptionalにtype predicateを使うことができます。

abstract class Optional<T> {
public abstract isPresent(): this is Some<T>;
}
class Some<T> extends Optional<T> {
public isPresent(): this is Some<T> {
return true;
}
}
class None<T> extends Optional<T> {
public isPresent(): this is Some<T> {
return false;
}
}

type predicateで注意すること

上記Optionalの例が顕著なのですが、optional.isPresent()falseを返したからと言ってTypeScriptは変数optionalNoneであるとは解釈しません。あくまでもSomeではないと解釈されるだけです。

if (optional.isPresent()) {
// optional is Some<T>
} else {
// optional is something else but Some<T>
}

またtype predicateはfalseの場合を定義することができません。つまり以下のような定義はできません。

public abstract isPresent(): this is Some<T>, this is not None<T>;

このような時は専用のメソッドを用意します。

abstract class Optional<T> {
// ...
public abstract isPresent(): this is Some<T>;
public abstract isAbsent(): this is None<T>;
}

ただしこの例の場合TypeScriptではユニオン型によって簡単に解決できます。ユニオン型については詳細の説明があるのでそちらをご参照ください。

戻り値のthis

四則演算ができる変哲もないクラスOperatorを考えます

class Operator {
protected value: number;
public constructor(value: number) {
this.value = value;
}
public sum(value: number): void {
this.value += value;
}
public subtract(value: number): void {
this.value -= value;
}
public multiply(value: number): void {
this.value *= value;
}
public devide(value: number): void {
this.value /= value;
}
}
const op: Operator = new Operator(0);
op.sum(5); // 5
op.subtract(3); // 2
op.multiply(6); // 12
op.devide(3); // 4

演算ごとにステートメントを分ける必要があります。 このような場合メソッドチェインを使って処理を連続させることができます。

class Operator {
protected value: number;
public constructor(value: number) {
this.value = value;
}
public sum(value: number): Operator {
this.value += value;
return this;
}
public subtract(value: number): Operator {
this.value -= value;
return this;
}
public multiply(value: number): Operator {
this.value *= value;
return this;
}
public devide(value: number): Operator {
this.value /= value;
return this;
}
}
const op: Operator = new Operator(0);
op.sum(5).subtract(3).multiply(6).devide(3); // 4

op.sum(), op.subtract(), op.multiply(). op.devide()の戻り値の型をOperatorに変更しました。これによりメソッドチェインが可能になりました。

ここで、このクラスOperatorを拡張して累乗の計算を追加したいとします。すると新しいクラスNewOperatorは以下のようになるでしょう。

class NewOperator extends Operator {
public constructor(value: number) {
super(value);
}
public power(value: number): NewOperator {
this.value **= value;
return this;
}
}

ですが、このクラスでは以下の演算ができません。

const op: NewOperator = new NewOperator(2);
op.power(3).multiply(2).power(3);
// Property 'power' does not exist on type 'Operator'.

これはop.multiply()の戻り値がOperatorだからです。Operatorにはpower()というメソッドがないためこのような問題が発生します。

このような時、戻り値にthisを設定することができます。上記クラスの戻り値のOperator, NewOperatorを全てthisに置き換えると問題が解消されます。

class Operator {
protected value: number;
public constructor(value: number) {
this.value = value;
}
public sum(value: number): this {
this.value += value;
return this;
}
public subtract(value: number): this {
this.value -= value;
return this;
}
public multiply(value: number): this {
this.value *= value;
return this;
}
public devide(value: number): this {
this.value /= value;
return this;
}
}
class NewOperator extends Operator {
public constructor(value: number) {
super(value);
}
public power(value: number): this {
this.value **= value;
return this;
}
}
const op: NewOperator = new NewOperator(2);
op.power(3).multiply(2).power(3); // 4096

オーバーロード(Overload)

オーバーロードとは、関数の名称は同じでありながら異なる引数、戻り値を持つことができる機能です。TypeScriptもこの機能を用意しているのですが、大元がJavaScriptであることが災いし、やや使いづらいです。

オーバーロードの定義

オーバーロードはその関数が受け付けたい引数、戻り値の組を実装する関数の上に書きます。例えば先ほど使用した2点の距離を求める関数distance()をオーバーロードで定義すると以下のようになります。なお、この例では戻り値の型は全てnumber型ですが、別の型にしても実装さえできれば他の型にしても問題ありません。

function distance(p: Point): number;
function distance(p1: Point, p2: Point): number;
function distance(x: number, y: number): number;
function distance(x1: number, y1: numebr, x2: number, y2: number): number;

なお、上記のような書き方のオーバーロードは名前付き関数またはクラスのメソッドでのみ可能です。匿名関数、アロー関数では、タイプエイリアスまたはインターフェースでオーバーロードを定義します。たとえば、上記例だと以下のようなタイプエイリアスになります。

type Distance = {
(p: Point): number;
(p1: Point, p2: Point): number;
(x: number, y: number): number;
(x1: number, y1: number, x2: number, y2: number): number;
};
const distance: Distance = (arg1: number | Point, arg2?: number | Point, arg3?: number, arg4?: number): number => {
// ...
};

オーバーロードの実装

ここからが大変です。実装はオーバーロードで定義した全てをひとつの関数で処理しなければいけません。つまりdistance()の実装は以下のようになります。これが呼び出し側ではあたかも他言語のオーバーロードのようになります。

function distance(p: Point): number;
function distance(p1: Point, p2: Point): number;
function distance(x: number, y: number): number;
function distance(x1: number, y1: numebr, x2: number, y2: number): number;
function distance(
arg1: Point | number,
arg2?: Point | number,
arg3?: number,
arg4?: number
): number {
// ...
}
distance(q1);
distance(q1, q2);
distance(1, 3);
distance(1, 3, 5, 7);

オーバーロードでうれしいこと

オーバーロードを定義しないで実装する、つまりオーバーロードの定義なしに実装すると以下のような引数を考慮しなければなりません。

distance(q1, 5, undefined, 8);

オーバーロードを定義しておくことで意図する引数と戻り値の組み合わせを定義できるようになります。上記引数はオーバーロードの定義によりTypeScriptから指摘を受けます。

Argument of type 'Point' is not assignable to parameter of type 'number'.

これはTypeScriptがdistance()number型の引数4個版で受けていると解釈している時の指摘です。