TypeScriptの進化の道のりとバージョン2.0の新機能(型システム編):特集:TypeScript 2.0概説(2/3 ページ)
2016年9月にTypeScript 2.0がリリースされた。本稿では、これまでのTypeScriptの進化の過程とバージョン2.0で導入された新機能を見てみよう。
[TS2.0]タグ付き共用型
タグ付き共用型(Tagged Union Types)とは、共通のタグを持った異なる型のオブジェクトを共用型にまとめることで、コードを簡潔に書けるようにするものだ。以下に例を示す。
interface Cat { // Cat型
kind: "cat"; // タグ
meow: () => void; // meowは引数なし、戻り値なしの関数
}
interface Dog { // Dog型
kind: "dog"; // タグ
bowwow: () => void; // bowwowは引数なし、戻り値なしの関数
}
type Animal = Cat | Dog;
var cat: Cat = { // Cat型のオブジェクト
kind: "cat",
meow: function() { console.log("meow");}
}
var dog: Dog = { // Dog型のオブジェクト
kind: "dog",
bowwow: function() { console.log("bow wow"); }
}
function hello(x: Animal) {
if (x.kind === "cat") {
x.meow();
} else {
x.bowwow();
}
}
hello(cat); // meow
hello(dog); // bow wow
ここではインタフェースを利用してCat型とDog型を定義して、それらを共用型Animalにまとめている。2つの型に共通するプロパティとしてkindがあることに注目してほしい。これがタグ付き共用体におけるタグとして機能する。気を付けたいのは、kindプロパティの値はTypeScript 1.8で導入された「文字列リテラル型」と呼ばれるものであることだ。
文字列リテラル型は、その名の通り、文字列リテラルを型として扱えるようにするものだ。つまり、Cat型が持つkindプロパティは"cat"型に、Dog型が持つkindプロパティは"dog"型になる。そして、そのプロパティに代入できるのは、その文字列リテラル型を構成する文字列と完全に一致する文字列のみとなる。つまり、Cat型のインスタンスであるcatのkindプロパティには"cat"のみを、Dog型のインスタンスであるdogのkindプロパティには"dog"のみを代入できるということだ(上のコードでは加えて、前者のmeowプロパティ、後者のbowwowプロパティに「引数なし、戻り値なし」のメソッドも設定している)。
文字列リテラルを型とすることのメリットについては、TypeScriptのハンドブック内の「String Literal Types」などを参照されたい。
関数helloは共用型Animalをパラメーターに受け取り、その実際の型をタグを基に判断し、それぞれの型に応じて、meowメソッドかbowwowメソッドを呼び出すようにしている。
今見たように、タグ付き共用型と文字列リテラル型(と型エイリアス)を組み合わせることでも、ポリモルフィックな関数を記述できるようになる。ポイントは次の3つだ。
- 複数の型に「文字列リテラル型で共通の名前のプロパティ」を持たせる
- 型エイリアスを利用してそれらを1つの共用型にまとめる
- 型ガード(上のコードではif文)でタグを基に実際の型を識別する
なお、上記のif文はswitch文を使用しても記述できる。
function hello(x: Animal) {
switch (x.kind) {
case "cat":
x.meow();
break;
case "dog":
x.bowwow();
break;
}
}
次にTypeScript 2.0で導入された「制御フローベースの型解析」についても見ておこう。見た目的にはここまでに見てきた型ガードとよく似た感じだ。
[TS2.0]制御フローベースの型解析
TypeScript 1.8では制御フローの解析が行われるようになったが、2.0ではこれをさらに推し進めて、制御フローの進行に伴う型推測が行われるようになった。ちょっと意味が分からないのだが、“コード明瞭意味不明”なサンプルを以下に示す。
function foo(x: number | number[]) {
if (typeof x === "number") {
console.log(x); // xはnumber型
return;
}
x.forEach(x => console.log(x * 2)); // if節中でreturnしているので、xはnumber[]型
}
foo(1);
foo([1, 2, 3]);
やっていることに意味はないのだが、関数fooはnumber型もしくはnumber[]型(numberの配列)をパラメーターに受け取る。前者を受け取った場合はそれをコンソールに出力し、後者を受け取った場合は要素を2倍したものをコンソールに出力していく。ここでは型ガードが機能するので、if節内部ではxはnumber型になる。そして、そこでreturnをしているので、if文を抜けた後にはxはnumber[]型となる。このような制御フローを考慮して、型推測を行うのが「制御フローベースの型解析」だ。試しに、上のコードから「return;」行を抜くとどうなるだろうか。
return文がないので、制御フロー的にはパラメーターxの型はif節を抜けたところで「number | number[]」型(共用型)になってしまう。そのため、上の画像に示したように、「x.forEach」呼び出しがエラーとなる(パラメーターxがnumber型の可能性があるので、安直にforEachメソッドは呼び出せない)。制御フローまで考慮した型推測を処理系が行ってくれるので、早期にバグが発見できるようになるはずだ。
その一方で、制御フローを考慮することで、コード自体も簡潔に表記できるようになる。例えば、TypeScriptの関数やメソッドでは省略可能なパラメーターを指定できる。よって、省略可能なパラメーターがあり、呼び出し時に確かにそれが指定されなかった場合には、何らかの対処が必要になる。以下に例を示す。
function hoge(x?: string) {
if (typeof x === "undefined") {
x = "foo"
}
console.log(x.toUpperCase());
}
hoge();
hoge("hoge");
省略可能なパラメーターが省略されると、その値は「undefined」となる。上のコードではこれを利用して、省略されたらパラメーターxの値を設定するようにしている。このように前処理をしてやることで、制御フローベースの型解析の結果、if文を抜けた後には、パラメーターxが文字列を所持していることが保証でき、そのためtoUpperCaseメソッドを呼び出す前にパラメーターの値をチェックし直す必要がなくなり、コードを簡潔に記述できる。
Copyright© Digital Advantage Corp. All Rights Reserved.