合法 TypeScript 第4章 関数
Tweet前章では TypeScript の type system の基礎を扱いました。つまり primitive type, object, array, tuple, enum, それから TypeScript が type を推測する方法、type のアサインがどのように機能するかといったことです。さあお次は TypeScript のメインディシュである(もしくはあなたが関数プログラマーであるならば、レゾンデートル、つまり存在意義といっていいかもしれない)関数についてです。
- 関数の宣言と呼び出しにおける JavaScript と TypeScript の違いについて
- overloading 機能について
- Polymorphic functions
- Polymorphic type aliases
関数の宣言と呼び出し
JavaScript において、関数は first-class object です。つまり、関数はオブジェクトと全く同じように扱うことができるということです。例えば変数にアサインしたり、他の関数に渡したり、関数の返り値として使ったり、オブジェクトにアサインしたり、プロトタイプにアサインしたり、プロパティを関数にしたり、プロパティを書き換えたり読み出したりすることができます。JavaScript において関数ができることはたくさんありますが、TypeScript はその全てを豊かな type system を用いて構造化します。
まずは TypeScript の関数がどのような見た目なのかご覧ください。
function add(a: number, b: number) {
return a + b;
}
関数の引数に対しては通常 annotation を加えます。(この例では a と b に対して annotation を加えていますね)TypeScript は関数の body 部分に対しては type を推測しますが、特別な場合を除けば基本的に「引数に対しては」推測を行いません。特別なケースというのは context から type が推測できる場合です。(これについては Contextual Typing で扱います)返り値は推測されますが、明示的に annotation を加えることも可能です。
function add(a: number, b: number): number {
return a + b;
}
ちょっとしたメモがき:本書では返り値に対して annotation を付与していますが、これはあくまで読者が関数が何をしているのか理解する上で助けになると思ったからにすぎません。しかし実際には TypeScript が推測してくれますので、わざわざ繰り返す必要はありません。
TypeScript で関数を表す方法は少なくとも5つあります。まずはこれらを紹介しましょう。
// Named function
function greet(name: string) {
return "hello " + name;
}
// Function expression
let greet2 = function(name: string) {
return "hello " + name;
};
// Arrow function expression
let greet3 = (name: string) => {
return "hello " + name;
};
// Shorthand arrow function expression
let greet4 = (name: string) => "hello " + name;
// Function constructor
let greet5 = new Function("name", 'return "hello " + name');
function constructor 以外の(これは全く型安全ではないので、ハチに追われてでもいない限り使うべきではない)関数を定義するシンタックスは全て TypeScript がサポートしており type safe に運用することができます。これらに関しては今までの通り、引数に対しては annotion を付与し、返り値に関しては付与してもしなくてもいいでしょう。
TypeScript において関数を呼び出すとき、追加の型情報を提供する必要はありません。単に引数を渡すだけで、TypeScript は仕事を開始し、その引数が、定義した関数の引数に適合するかをチェックします。
add(1, 2); // evaluates to 3
greet("Crystal"); // evaluates to 'hello Crystal'
もちろん、引数を渡し忘れたり、誤った type の引数を渡せば、TypeScript が即座に指摘してくれます。
add(1); // Error TS2554: Expected 2 arguments, but got 1.
add(1, "a");
// Error TS2345: Argument of type '"a"' is not assignable
// to parameter of type 'number'.
Optional parameter と default parameter
object や tuple のときと同じく ?
を用いることでパラメーターを optional にすることができます。ただし必須のパラメーターを先に配置して、optional な引数はそのあとに続ける必要があります。
function log(message: string, userId?: string) {
let time = new Date().toLocaleTimeString();
console.log(time, message, userId || "Not signed in");
}
log("Page loaded"); // Logs "12:38:31 PM Page loaded Not signed in"
log("User signed in", "da763be"); // Logs "12:38:31 PM User signed in da763be"
JavaScript と同じく、default 値を optinal parameter に与えることも可能です。単純な optional の場合と同じく呼び出し時に引数を与えないことができます。(違いがあるとすれば、単純な optinal の場合には引数リストの最後に必ず配置しなくてはいけなかったのですが、default paramter の場合はそうする必要はありません)
例えばこんなふうに書き直すことができます。
function log(message: string, userId = "Not signed in") {
let time = new Date().toISOString();
console.log(time, message, userId);
}
log("User clicked on a button", "da763be");
log("User signed out");
もちろん明示的に default parameter に対して type annotation を与えることもできます。
type Context = { appId?: string; userId?: string };
function log(message: string, context: Context = {}) {
let time = new Date().toISOString();
console.log(time, message, context.userId);
}
optional よりも default parameter を使う機会の方が多いでしょう。
Rest Parameters
argument のリストを受け付ける関数があったとしましょう。もちろんこの関数に対しては配列としてリストを渡すだけで機能します。
function sum(numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sum([1, 2, 3]); // evaluates to 6
時には variadic function にたしたい場合もあるでしょう。variadic function とは、引数の数が可変である関数です。反対に fixed-arity な関数というのは、引数の数が固定されています。variadic function にするためには JavaScript は伝統的に arguments object を用いてきました。
arguments object は少し特殊で、JavaScript ランタイムが実行時に自動的にこれを定義します。関数に渡した引数を arguments object にアサインするのです。arguments object は本当の配列ではなく、配列の「ような」ものでしかないので、まずはこれを配列に変換する必要があります。
function sumVariadic(): number {
return Array.from(arguments).reduce((total, n) => total + n, 0);
}
sumVariadic(1, 2, 3); // evaluates to 6
しかし arguments object を使う大きな問題が一つだけあります。それは、arguments object は全く type safe ではないという点です。total もしくは n の上にマウスをホバーするとわかるのですが、これらはどちらも any になってしまうのです。
また関数 sumVariadic は引数を受け取るものであると宣言していないため、TypeScript はこの関数は引数を受けるものではないと判断しますので、これを使おうとすると TypeError が当然ですがおきます。
sumVariadic(1, 2, 3); // Error TS2554: Expected 0 arguments, but got 3.
では type safe に引数可変型の関数を用いるにはどうしたらいいのでしょうか?
そう、rest parameters の出番です!安全ではない aruguments object を用いるのではなく rest parameters を用いることで、安全に sum 関数は何個でも引数を受け取れる関数となります。
function sumVariadicSafe(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sumVariadicSafe(1, 2, 3); // evaluates to 6
これだけです。元の関数とこの関数の違いは「...」を使っている点だけです。それ以外には何も変えていませんが、これだけで完全に typesafe になるのです。
関数が持てる rest parameter は最大一つであり、またこれはかならず引数のリストの最後に位置する必要があります。例えば console.log に対する TypeScript の built-in declaration には(interface のことを今は知らなくても問題ありません。これについては後ほど説明します)、console.log が optinal の message という引数に加えて、引数の数が可変な pitonalParams を受け取れると次のように定義されています。
interface Console {
log(message?: any, ...optionalParams: any[]): void;
}
call, apply, bind
関数は「()」によって呼び出す以外にも、いくつかの方法が JavaScript には用意されています。
function add(a: number, b: number): number {
return a + b;
}
add(10, 20); // evaluates to 30
add.apply(null, [10, 20]); // evaluates to 30
add.call(null, 10, 20); // evaluates to 30
add.bind(null, 10, 20)(); // evaluates to 30
apply
は、関数内の this に対して値を bind する機能を持ちます。(上記の例では null と this に対して bind しています。)第二引数に配列を渡すと、それが関数の引数として展開されます。call
も同様ですが、第二引数に配列を渡すのではなく、一つづつ順番に渡します。
bind() もこれらに似ており、this へ bind する機能と引数を渡す機能も持っています。ただし、bind は関数を呼び出しません。呼び出すのではなく、bind をした新しい関数を返します。これに対して ()
, .call
, .apply
を使ってさらに引数を渡して呼び出すことも可能です。
this に型を与える
JavaScript 以外の世界からきた人には、this が全ての関数それぞれに定義されていることに驚くことでしょう。一般的な言語では class の中のメソッドの中で this を普通は使うと思いますが、JavaScript においては通常の関数の中でも this が定義されるのです。this は非常に特殊な存在で、その値は関数を呼び出した状況によって変わります。この性質ゆえに、悪名高き this を用いることによってアプリケーションは脆弱に、そして理解しづらいものになってしまうのです。それゆえ、多くの開発チームでは class method 以外での this の使用を禁止しています。TSLint ルールに no-invalid-this を追加することで実現可能です。
this が脆弱性をもたらすケースを見ていきましょう。まずは値を取り出すタイミングによる問題です。一般的に JavaScript のオブジェクトの値を取り出す場合には以下のようにドットを用いて行います。
let x = {
a() {
return this;
}
};
x.a();
// this is the object x in the body of a()
しかしどこかの段階で、呼び出す前にこのメソッドを他の変数に割り当てると、結果が変わってしまうのです!
let a = x.a;
a(); // now, this is undefined in the body of a()
もう一つ例を出しましょう。日付を整えて返す関数です。
function fancyDate() {
return `${this.getDate()}/${this.getMonth()}/${this.getFullYear()}`;
}
おそらくこの関数はまだ経験の浅い時期にあなたが書いたものでしょう。(まだあなたは引数を関数が受け取れることを知らなかったのです。)この関数を使うためには、Data
を this に対して bind しなくてはいけません。
fancyDate.call(new Date()); // evaluates to "4/14/2005"
もし this に bind するのを忘れてしまうと、runtime exeption が生じてしまいます!
fancyDate(); // Uncaught TypeError: this.getDate is not a function
this がどうなっているのかを本書で完全に説明することはできませんが、this の値は、定義した時ではなく、呼び出した時に決定するというこの挙動は、控えめに言って人間が予測できるような代物ではありません。
ありがたいことに TypeScript があなたを守ってくれます。this を使った関数を定義した場合には、第一引数に期待する this の type を宣言します。(その後に続けて optinal parameter を使うことも可能です)すると TypeScript は this があなたの指定したものになっているのか、呼び出される場所全てで確認をしてくれるのです。この場合の this は他の引数とは異なる特別なものです。関数内で使う場合には予約語として機能しています。
function fancyDate(this: Date) {
return `${this.getDate()}/${this.getMonth()}/${this.getFullYear()}`;
}
では実行してみましょう。
fancyDate.call(new Date());
// evaluates to "6/13/2008"
fancyDate();
// Error TS2684: The 'this' context of type 'void' is // not assignable to method's 'this' of type 'Date'.
こうすることでコンパイル時に TypeScript が問題を報告してくれるのです。
関数内で常に this に明示的に annotation をつけることを強制するためには、noImplicitThis を tsconfig.json に追加します。strict mode の場合にはそれが含まれているので追加する必要はありません。
Generator Functions
Generator functions は(短く generators と呼ぶこともあります)、一連の値を generate = 生成する便利な手段です。generator が値を生成するタイミングは、それを使用する側からコントロールすることができます。この性質によって他の手段では実現しにくい、例えば無限に続くリストなどを生成することができます。値の算出を、generator を使用する側が要求するまで保留する、という機構によってこれは達成されています。
function* createFibonacciGenerator() {
let a = 0;
let b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
let fibonacciGenerator = createFibonacciGenerator(); // IterableIterator<number>
fibonacciGenerator.next(); // evaluates to {value: 0, done: false}
fibonacciGenerator.next(); // evaluates to {value: 1, done: false}
fibonacciGenerator.next(); // evaluates to {value: 1, done: false}
fibonacciGenerator.next(); // evaluates to {value: 2, done: false}
fibonacciGenerator.next(); // evaluates to {value: 3, done: false}
fibonacciGenerator.next(); // evaluates to {value: 5, done: false}
(*)
アスタリスクを関数名の前につけることで、その関数を generator fcuntion とすることができます。この generator 関数を呼び出すと、iterable iterator が生成され return されます。- 上記の関数は、永遠に値を生成し続けます。
- yield キーワードを generator の中でも用いることで、値を生じさせることができます。generator を用いる側が「次の値を要求すると」(例えば .next() を実行すると)yield は用いる側に算出結果の値を返し、同時に次に値が要求されるまではそこで実行を一旦停止します。そのため、たとえ while を用いた無限ループであるにも関わらず、即座に無限に実行されないので、プログラムをクラッシュすることにはなりません。
- つぎのフィボナッチ数を演算するために、a には b を、b には a + b をアサインしています。
createFibonacciGenerator を呼び出すと、IterableIterator が返ってきます。IterableIterator に対して next を実行するたび、iterator はつぎのフィボナッチ数を算出し、yield の部分で値を返します。TypeScript は iterator が yield によって返す値をみて、返り値の type を推測します。
もちろん明示的に generator に annotation を追加することもできます。そのためには IterableIterator
で type をラップします。
function* createNumbers(): IterableIterator<number> {
let n = 0;
while (1) {
yield n++;
}
}
let numbers = createNumbers();
numbers.next(); // evaluates to {value: 0, done: false}
numbers.next(); // evaluates to {value: 1, done: false}
numbers.next(); // evaluates to {value: 2, done: false}
generators を深く掘り下げることを本書では行いませんが、この機能はとにかく最高で、もちろん TypeScript も完全にそれをサポートしてくれています。より generators について探求したい場合には MDN の Generators の項目 をご覧ください。
Iterators
Iterator は generator と対を成す存在です。generator は連続する値を生成する手段ですが、iterator はそれを「使用」する手段です。専門用語が多くわかりにくいと思うので、まずはこれらの定義を説明しましょう。
Iterable とは:Symbol.iterator で呼び出せるプロパティを持ったオブジェクトで、そのプロパティの値が「iterator を返す関数」であるもの。
Iterator とは:オブジェクトのうち、next で呼び出せるメソッドを持っており、それを実行すると value と done というプロパティを持ったオブジェクトを返すもの。
Generator を作ると(つまり先ほどの createFibonacciGenerator を実行すると)iterable かつ iterator である値が返ってきます。この値がつまり iterable iterator です。なぜならば Symbol.iterator プロパティを持ってpり、かつ next メソッドも持った値だからです。
Generator を使わずとも自分で iterator もしくは iterable を作成することも可能です。オブジェクトもしくはクラスに、Symbol もしくは next を実装してあげればいいのです。例えば iterator を定義してみましょう。これは 1 から 10 までの値を返します。
let numbers = {
*[Symbol.iterator]() {
for (let n = 1; n <= 10; n++) {
yield n;
}
}
};
numbers は [Symbol.iterator]
で呼び出せるプロパティを持ち、その値が関数で、これを numbers[Symbol.iterator]()
と実行すると iterable iterator を返します。
自分自身で iterator を定義できるだけではなく、JavaScript に最初から組み込まれている iterator を使うこともできます。例えば Array, Map, Set, String などです。これらに対しては以下のような操作が可能です。
// Iterate over an iterator with for-of
for ( let a of numbers )
{ // 1, 2, 3, etc. }
// Spread an iterator
let allNumbers = [... numbers ]
// number[] // Destructure an iterator
let [ one , two , ... rest ] = numbers
// [number, number, number[]]
Call Signatures
今までは関数の引数と返り値の型について話をしてきました。ここからは関数全体の型定義をしていく方法をみていきましょう。
以前扱った sum 関数を再度取り上げます。
function sum(a: number, b: number): number {
return a + b;
}
この関数 sum の type は何でしょうか?sum は関数ですから type は Function
になります。しかしこの Function
type は、おそらく使いたいと思わないはずです。なぜなら、object
type が全ての object を表していたのと同じく、Function
タイプは全ての関数を表しているだけで何の情報をも提供してくれないからです。
ではどうすれば適切にこの関数の type を表すことができるでしょうか。この関数は二つの number type の引数を持ち、返り値mの number type です。TypeScript においてはこれは以下のように表現することができます。
(a: number, b: number) => number;
これは TypeScript の function type のためのシンタックスで、call signature とも呼ばれます。ほとんどアロー関数と同じ形であることに気づいたでしょう。そう、実はこれは意図的にそうなっているのです。引数として関数を渡す場合や、返り値として関数が返ってくるときにはこれを使って定義します。
Function call signatures は type-level のコードだけが含まれている。これはつまり type に関する情報だけで、値に関してのコードは含まれていないということです。ということは function call signatures が表現できるのは、parameter type, this type, return type, rest type, optional type だけであって、default value は表現できません。(なぜなら default value は value であって type ではないからです。)またボディ部分がないので TypeScript が推測する return type が存在しないので、return type は明示的に annotation を加える必要があります。
ではこの章で取りあげた関数を参考にして、その関数の type だけを単独の call signature として取り出し、これを type alias に bind していきましょう。
// function greet(name: string)
type Greet = (name: string) => string;
// function log(message: string, userId?: string)
type Log = (message: string, userId?: string) => void;
// function sumVariadicSafe(...numbers: number[]): number
type SumVariadicSafe = (...numbers: number[]) => number;
だいたい感じは掴めたでしょう?関数の call signatures は本当に関数の実装そのものと似ているのです。これは意図的なものであり、call signature を理解しやすくする意図を持って設計されています。
では call signatrues とその実装に関係性についてみていきましょう。call signature をまず定義したら、この signature を実装した関数を宣言します。そのためには call signature と関数の実装を組み合わせるだけです。例えば以前紹介した log 関数を call signature と組み合わせて実装してみましょう。
type Log = (message: string, userId?: string) => void;
let log: Log = (message, userId = "Not signed in") => {
let time = new Date().toISOString();
console.log(time, message, userId);
};
- log という関数を定義するときに Log type を使って明示的に型を定義します。
- 引数についてはすでに Log を使って定義しているので、関数実装の際に再度 type を明示的に annotation する必要はありません。
- 関数を実装する際に userId に対して default value を定義しています。call signature では optinal な value として userId があることは定義できても、その値に関する情報は定義できないからです。
- 返り値についても再度定義する必要はありません。なぜなら Log type で既に void であると定義しているからです。
Contextual Typing
さきほど紹介した log 関数が、実は本書ではじめて引数に関して annotation を付与しなかった関数です。それでも問題がなかったのは、log 関数は type Log であると定義したことによって、TypeScript が context に基づいて判断し、message が string type であると推測できるからです。このような TypeScript による推測は contextual typing と呼ばれる機能です。
実はこの章の最初の方で少しだけ contextual typing が出現しました。それはコールバック関数を使ったケースです。
では times というコールバックを実行する関数を定義してみましょう。この関数はコールバックとして渡した f という関数を n 回実行します。その際、コールバックに対して現在何回目なのかというインデック = n を渡します。
function times(f: (index: number) => void, n: number) {
for (let i = 0; i < n; i++) {
f(i);
}
}
times を実行する際に、times に渡す関数について明示的に annotation を加える必要はありません。ただしインラインで関数を宣言している場合のみですが。
times(n => console.log(n), 4);
TypeScript は context から判断して n は number type であると判断します。なぜならば、関数 f の引数である index は number であると times 関数内の signature で定義しているので、TypeScript は n がそれに対応する引数であるから number になる、と推測するのです。非常に賢いですね。
注意して欲しいのですが、f 関数をインラインで定義しない場合には、TypeScript はその type を推測してくれません。
function f(n) {
// Error TS7006: Parameter 'n' implicitly has an 'any' type.
console.log(n);
}
times(f, 4);
Overloaded Function Types
実は先のセクションで使用した type Fn = (...) => ...
というシンタックスは、略記版 call signature で、より明示的に記述するシンタックスもあります。
// Shorthand call signature
type Log = (message: string, userId?: string) => void;
// Full call signature
type Log = { (message: string, userId?: string): void };
これら二つは単にシンタックスが異なる以外は、完全に同じものです。
基本的に Log function のようなシンプルなケースにおいては略記バージョンの方を使ったほうがいいと思いますが、複雑な関数の場合には full call signatures の方を使ったほうがいい場合があります。
そういったケースの一つが function type を overloading する場合です。これについて説明する前に、まずは function を overload するのとは何なのか説明しましょう。
Overloaded Function:関数のうち、call signature(引数の数や型、さらに返り値の型)が複数パターンあるもの
ほとんどのプログラミング言語では、関数を一旦定義したら、その定義の際に決めた引数のセットでのみ関数を呼び出すことができますし、返り値の型も常に一定になります。しかし JavaScript はそうではありません。JavaScript は非常に動的な言語なので、関数を呼び出す方法が複数あるのが普通ですし、引数によって返り値の型が変わることもよくあることなのです。