第2章 コードの表現力を高める応用的な型
実際の開発で型を付けていくと、「記述が煩雑だ」と感じたり、特定のケースで「どのように型を表現すればよいかわからない」といった課題に直面したりします。そうした時のヒントになるTypeScriptの各種機能を紹介していきます。
ジェネリクス
ジェネリクスは、総称型と呼ばれるものです。型が出てくるプログラミング言語では、関数の引数に型を書きます。しかし、型だけが違う似た関数が大量に作られるというデメリットもあります。
ジェネリクスはこうした不便を解消してくれます。関数の引数で型を決め打ちせず、どの型なのかの情報を渡して関数を実行できます。
それではTypeScriptのジェネリクスの例を見ていきましょう。初期値がn個連続している配列を作るコードを考えてみます。初期値の型は不明です。まずは、ジェネリクスを使わずに、any型で作ってみます。
function initFillArrayAny(val: any, repeat: number): any[] {
return new Array(repeat).fill(val);
}
let numbersAny: number[] = initFillArrayAny(123, 4);
let stringsAny: string[] = initFillArrayAny("hoge", 4);
let invalidAny: string[] = initFillArrayAny(123, 4);
console.log(invalidAny[0].split('')); // エラーが起きる
initFillArrayAny関数では、引数valにどんな型が入るのか分からないために、any型としています。
このコードは、TypeScriptではエラーが表示されません。しかしJavaScriptにして実行すると、最後のinvalidAny[0].split('')でエラーが起きます。
initFillArrayAny(123, 4)で生成される配列は、数値が連続する[123, 123, 123, 123]ですが、変数invalidAnyに付けた: string[]の型注釈でエラーを検出できていません。もっと厳密に型を検査する必要があります。
次は、型を指定した関数を複数作ったケースです。
function initFillArrayString(val: string, repeat: number): string[] {
return new Array(repeat).fill(val);
}
function initFillArrayNumber(val: number, repeat: number): number[] {
return new Array(repeat).fill(val);
}
let numbersNumber: number[] = initFillArrayNumber(123, 4);
let stringsString: string[] = initFillArrayString("hoge", 4);
let invalidString: string[] = initFillArrayString(123, 4); // エラーになる
こうすると、initFillArrayString(123, 4)でエラーが検出されます。実行時にinvalidAny[0].split('')でエラーが起きることを防げます。
ただ、このように書くと、型ごとに関数を書く必要があり煩雑です。この手間を減らすのがジェネリクスです。型自体を変数にして型変数名を記述してコードをまとめます。例を示します。
function initFillArray<T>(val: T, repeat: number): T[] {
return new Array(repeat).fill(val);
}
let numbers: number[] = initFillArray(123, 4);
let strings: string[] = initFillArray("hoge", 4);
let invalid: string[] = initFillArray(123, 4); // エラーになる
上のコードでは、最後の行のinvalidでエラーが出ます。エラーの内容はType 'number[]' is not assignable to type 'string[]'.(型 'number[]' は型 'string[]' に代入できません。)です。
関数の名前のあとに<T>と書くことで、型変数Tが使えます。<T>は型変数名の定義です。Tは慣習的に使われる文字です。for文のiのようなものです。
ここでは、引数valの型Tを、返り値でT[]のように利用します。引数valの型がnumberなら、返り値の型はnumber[]になります。引数valの型がstringなら、返り値の型はstring[]になります。
上記のinitFillArray関数を使うコードは、次のようにも書けます。
let numbers2: number[] = initFillArray<number>(123, 4);
let strings2: string[] = initFillArray<string>("hoge", 4);
let invalid2: string[] = initFillArray<string>(123, 4); // エラーになる
この場合は、最後の行の123でエラーが出ます。エラーの内容はArgument of type 'number' is not assignable to parameter of type 'string'.('number' 型の引数は 'string' 型のパラメータに割り当てることができません。)です。こちらの方が、より早い段階でエラーを検出できています。
<number>や<string>は型引数と呼ばれるものです。これらを省略すると、型引数の型推論が行われます。
また、型変数に代入可能な型はextendsを利用して限定できます。例を示します。
function initFillArraySN<T extends string | number>(val: T, repeat: number): T[] {
return new Array(repeat).fill(val);
}
let numbersSN: number[] = initFillArraySN(123, 4);
let stringsSN: string[] = initFillArraySN("hoge", 4);
let booleansSN: boolean[] = initFillArraySN(true, 4); // エラーになる
ここではstringとnumberに限定しています。そのため最後のようにbooleanを引数に書くとエラーが発生します。
ジェネリクスには他にも応用的な機能がありますが、まずは基本を理解し、開発で不便を感じた際に、必要に応じて学習を進めていくとよいでしょう。
Promiseの型指定
TypeScriptでPromiseを利用するときは少し注意が必要です。
JavaScriptで頻繁に利用される、Promiseを返す関数を例に見ていきましょう。この例は、TypeScriptではエラーが出ます。
function waitAndGet(time: number): Promise {
return new Promise(resolve => {
setTimeout(() => {
let n: number = Math.trunc(Math.random() * 100);
resolve(n);
}, time);
});
}
VSCode上でPromiseに赤線が引かれてエラーが検出されます。なぜでしょうか。
警告が出ているPromiseにマウスカーソルを重ねてエラーの内容を見てみます。Generic type 'Promise<T>' requires 1 type argument(s).(ジェネリック型 'Promise<T>' には 1 つの型引数が必要です。)と書いてあります。
次に「Ctrl」を押しながら、このPromiseをクリックします。interface Promise<T> { ~ }と表示されます。TypeScriptでは、このPromiseはインターフェースで、宣言する時に型引数を要求していることが分かります。
そこで次のように直すと問題が解決します。
function waitAndGet(time: number): Promise<number> {
return new Promise(resolve => {
setTimeout(() => {
let n: number = Math.trunc(Math.random() * 100);
resolve(n);
}, time);
});
}
Promise<number>のようにresolve()で送る値の型を書きます。ここではnumberなので<number>と書きます。
この関数を使う例も示します。
(async function() {
let attack: number = await waitAndGet(1000);
console.log({attack});
let defense: number = await waitAndGet(1000);
console.log({defense});
})();
型アサーション
次に型アサーションを紹介します。型推論を上書きする機能です。asを利用して、「~としてあつかう」というプログラマーの意図をTypeScriptのコンパイラに伝えます。
例を示します。
let obj = JSON.parse('{"name": "Tom"}');
let val: string | number = obj.name;
let len: number = val.length; // エラーになる
上のコードは、TypeScriptではエラーが出ます。JSON.parseはany型を返します。そのためobjもnameもany型です。val: string | numberに代入した状態では、valの型はstring | numberになります。そのため、val.lengthのように文字列のプロパティを得ようとするとエラーが出ます。
本来なら、JSON.parseで得たあとに、型ガードで型を絞り込むべきですが、今回は例なので割愛します。
プログラマーの視点からすると、上のコードのvalが文字列であることが明確です。そこでasを利用して次のように書くことができます。
let obj = JSON.parse('{"name": "Tom"}');
let val: string | number = obj.name;
let len: number = (val as string).length;
変数valをstringとしてあつかうと書いているわけです。このように型推論を上書きすることで、エラーを避けることができます。
型アサーションにはもう一つ書き方があり、<>を利用して次のように書くこともできます。
let obj = JSON.parse('{"name": "Tom"}');
let val: string | number = obj.name;
let len: number = (<string>val).length;
この型アサーションは、キャスト(型の変換)とは違います。コンパイルした時に除去されるTypeScriptの型にまつわる機能です。
また、型アサーションで行えるのは可能性の絞り込みです。無理矢理の読み替えは行えません。次のようなプログラムはエラーになります。
let num: number = 123; let len4: number = (num as string).length; // エラーになる
