第2章 TypeScriptで型をあつかう
第1回の「TypeScriptで型を付ける」では、変数や関数に直接型を書いていきました。単純なプリミティブ型なら、こうした方法でも構いませんが、複雑な配列やオブジェクトでは、型の記述を再利用したくなります。
TypeScriptでは、型を変数のようにあつかえる方法があります。その方法をここでは見ていきます。
型エイリアス
TypeScriptでは、型に名前を付けて再利用できます。名前を付けた型は、型エイリアス(Type Alias)と呼びます。型エイリアスの宣言にはtypeを使います。
いくつかの例を示します。型エイリアスを作っておき、変数の宣言時に利用しています。使い方の雰囲気が分かると思います。
// 関数の戻り値の型を定義する
type MatchResult = RegExpMatchArray | null;
let m: MatchResult = "123".match(/\d+/);
// 取り得る値を定義する
type ResponseCode = 400 | 500 | 600;
let res: ResponseCode = 500;
// オブジェクトの構造を定義する
type User = { name: string, age: number };
let user: User = { name: "Tom", age: 16 };
// コールバック関数の構造を定義する
type CallbackNumberCheck = (value: number) => boolean;
function check(param: number, cb: CallbackNumberCheck): string {
let res = cb(param);
return res ? "Valid" : "Invalid";
}
console.log(check(123, n => n > 100));
オブジェクトのプロパティを読み取り専用にする
TypeScriptでは、オブジェクトのプロパティを細かく制御できます。まずは読み取り専用にするreadonlyです。
let user: {
name: string;
readonly id: number;
};
user = { name: "Tom", id: 1 };
user.name = "Bob";
user.id = 2; // エラーになる
上の例では、最後の行のuser.id = 2;でエラーが発生します。readonly idで読み取り専用にしているためです。
読み取り専用にする際は、いくつか注意すべき点があります。
1つ目は、この読み取り専用の属性は、静的解析をおこなうTypeScriptの中だけで有効です。コンパイル後のJavaScript側には読み取り専用という仕様はなく、変更が可能になります。
2つ目は、プロパティの値に配列やオブジェクトなどを指定した場合には、それらの要素は書き換えが可能です。これは、JavaScriptのconstで定数を作ったときと同じです。
例を示します。
let chara: {
name: string;
readonly skill: string[];
};
chara = { name: "Hero", skill: [] };
chara.skill[0] = "Sword"; // エラーにならない
chara.skill = ["Magick"]; // エラーになる
readonly skill: string[]として、文字列の配列を読み取り専用にしています。しかし、chara.skill[0] = "Sword"は、skillに代入した配列を別の配列に書き換えているわけではないので、エラーにはなりません。chara.skill = ["Magick"]は書き換えているのでエラーになります。
ネストしたプロパティや要素を読み取り専用にしたい場合は、ネストした側にもreadonlyを付けます。例を示します。
let monster: {
name: string;
readonly element: readonly string[];
readonly area: {
readonly main: string;
};
};
monster = {
name: "Frog",
element: ["Water", "Earth"],
area: {main: "Lake"}
};
monster.element[0] = "Wind"; // エラーになる
monster.area.main = "Mountain"; // エラーになる
上のコードでは、readonly string[]としているため、monster.element[0] = "Wind"はエラーになります。また、readonly mainとしているため、monster.area.main = "Mountain"もエラーになります。
readonly string[]の部分は、ReadonlyArray<string>と書くこともできます。
オブジェクトで要素が多い場合に、1つずつreadonlyを付けていくのは大変です。一括で付ける方法もあります。ユーティリティ型のReadonlyを使います。
let primitiveCube: Readonly<{
x: number;
y: number;
z: number;
}>;
primitiveCube = {x: 1, y: 1, z: 1};
primitiveCube.x = 2; // エラーになる
最後の行でprimitiveCube.xに値を代入しているのでエラーが起きます。
オブジェクトのプロパティを選択可能にする
TypeScriptでは、オブジェクトのプロパティを書いて型注釈を書くと、そこで書いた以外のプロパティを加えようとするとエラーが起きます。例を示します。
let position: { x: number, y: number };
position = { x: 100, y: 200, z: 300 };
上のコードでは、zのところでエラーが出ます。宣言していないプロパティを使用しているためです。
TypeScriptには「付けてもいいし、付けなくてもよい」という選択可能なプロパティを書く方法があります。オプションプロパティという方法です。プロパティ名のあとに?を書くと、選択可能なプロパティになります。
type Position = { x: number, y: number, z?: number };
let pos2d: Position = { x: 100, y: 200 };
let pos3d: Position = { x: 100, y: 200, z: 300 };
ただし、選択可能なプロパティにnullを設定することはできません。例を示します。
let posInvalid: Position = { x: 100, y: 200, z: null };
上のコードではzのところでエラーが起きます。VSCodeでこのzにマウスカーソルを重ねると、Type 'null' is not assignable to type 'number | undefined'.(型 'null' は型 'number | undefined' に割り当てることはできません。)とメッセージが出ます。
zプロパティに割り当て可能な型は、宣言したnumberと、未定義を表すundefinedになっていることが分かります。
オブジェクトのプロパティの形状を指定する
たとえば、オブジェクトのプロパティにさまざまなキー名で数値のパラメータを格納したいとします。その時に、全てのプロパティ名を指定するのが現実的ではないこともあります。
そうしたときに便利なのがインデックス型です。「キーを文字列、値を数値」のように指定して、利用可能なプロパティを設定できます。
例を示します。キーが文字列、値が数値のプロパティの型注釈です。
let params: {
[key: string]: number;
};
params = { hp: 10, mp: 15, level: 1 };
[key: string]: numberのところで、複数個の「キーが文字列、値が数値」のプロパティを意味しています。
keyのところは他のフレーズでも構いません。[K: string]: numberのように書くことも多いです。
この書き方は、通常のプロパティの書き方と混ぜて書いても構いません。例を示します。
let paramsWithId: {
id: number;
[key: string]: number;
};
paramsWithId = { id: 123, hp: 10, mp: 15, level: 1 };
この場合はidが必須になります。
インデックス型は、ユーティリティ型Recordを使って書くこともできます。
let paramsRecord: Record<string, number>;
paramsRecord = { hp: 10, mp: 15, level: 1 };
型の情報からキーを決定してプロパティ名を作ることもできます。Mapped Typesと呼ばれる機能です。
type ParamsKey = "hp" | "mp" | "level";
type Params = {
[key in ParamsKey] : number;
}
let myParams: Params = { hp: 10, mp: 5, level: 1 };
[key in ParamsKey]のところで、"hp" | "mp" | "level"の各値が展開されます。結果的に、次のコードと同じ意味になります。
type Params = {
hp : number;
mp : number;
level : number;
}
