始めに
この記事は群馬高専 Advent Calendar 2023 21日目の記事です。
今回はTypeScriptの個人的に面白い型システムについてざっくり紹介していきます。なので、基本的な型の詳細は今回は割愛させていただきます。
そもそもTypeScriptとは
TypeScriptとはJavaScriptという言語のスーパーセットとしてMicrosoft社によって開発された静的型付け言語です。この言語は「Type(型) + JavaScript」というコンセプトにそっているため、JavaScriptの構文をそのまま使用することが可能で、JavaScriptに型を書き加える言語であると言えます。
また、有名なエディタ Visual Studio Code (通称 VSCode) は同じくMicrosoft社の製品なので、TypeScriptの入力補完や構文・型チェックがデフォルトで使用できます。
TypeScriptの型システム
TypeScriptはより大規模なJavaScript開発に耐えられるようにするものなので、実行時にはJavaScriptに変換(トランスパイル)されて動きます。そのため、型システムはトランスパイルでのエラーチェックに使用されます。
例
/* 変数a を string型に定義 */
let a:string;
a = "str"; //OK
a = 3; //型 'number' を型 'string' に割り当てることはできません。
/* 関数abc の
引数a はnumber型
引数b はstring型
に定義
*/
function abc(a:number,b:string){
console.log(a,b);
}
abc(1,"slime");//OK
abc(1,26);//型 'number' の引数を型 'string' のパラメーターに割り当てることはできません。
abc("1","b");//型 'string' の引数を型 'number' のパラメーターに割り当てることはできません。
このように、型を定義しておけばコーディングミスを発見しやすくなり、変数や関数呼び出しなどさまざまな部分が型安全になります。
ほかにも、オブジェクトの構造を型に宣言しておくことで、記述の過不足を防ぐことができます。
interface Animal {
name: string;
age: number;
}
const dog:Animal = {
name: "pochi",
age: 8
};
const rabbit:Animal = {
age: 3,
};//プロパティ 'name' は型 '{ age: number; }' にありませんが、型 'Animal' では必須です。
const slime:Animal = {
name: "waruiSlime",
age: 404,
technique: "Splash"//オブジェクト リテラルは既知のプロパティのみ指定できます。'technique' は型 'Animal' に存在しません。
};
TypeScriptの型システムの面白いところ
では、そんなTypeScriptの型システムの中でも、特に有効なテクニックを2つ紹介していきます。
1. 型を配列から生成
次の例を見てください。
ちなみに型"bronze" | "silver" | "gold"
は"bronze"
"silver"
"gold"
のいずれかであることを表す、 ユニオン型 (直和型)です。
type rankType = "bronze" | "silver" | "gold";
interface User {
name: string;
rank: rankType;
}
const users:User[] = [];
function addUser(name:string,rank:string):number{
if(rank !== "bronze" && rank !== "silver" && rank !== "gold"){
console.log("Adding user failed.");
return -1;
}
return users.push({name,rank})-1;
}
この例では、名前とランクを持った「ユーザー」とその配列に追加する関数を定義しています。ただし、引数rankはrankType型以外の可能性もあります。そのため、引数rankがrankTypeを満たしているか確認する必要があります。型は実行時は参照できないので、if文のところで全ランクと比較して確認していますね。
ところが、ランクにプラチナ(platinum)が追加されたらどうでしょうか?
type rankType = "bronze" | "silver" | "gold" | "platinum";//"platinum"を追加
interface User {
name: string;
rank: rankType;
}
const users:User[] = [];
function addUser(name:string,rank:string):number{
if(rank !== "bronze" && rank !== "silver" && rank !== "gold" && rank !== "platinum"){//ここにも"platinum"を追加
console.log("Adding user failed.");
return -1;
}
return users.push({name,rank})-1;
}
一つの変更にあわせて複数箇所を変更することになりました。また、このif文はrankType型に直接影響されていないので、間違った記述をしてしまうかもしれません。
if(rank !== "bronze" && rank !== "silver" && rank !== "gold" && rank !== "diamond"){
//エラーはここではなく別の箇所で発生する
TypeScriptには、配列などをその要素のリテラルに型を固定するas const
と値からその型を抽出するtypeof
演算子があります。そのため、
const abc = ["a","b","c"] as const;//readonly ["a", "b", "c"] 型
type abcType = (typeof abc)[number];// 0:"a",1:"b",2:"c" -> "a"|"b"|"c"
とすると、as const
で配列の型をリテラルに固定しtypeof
でその型を取得、number型でアクセスすると要素のいずれかが選択されるという考えから全要素のユニオン型
このとき、以下のようにするとrankType型の定義と確認方法が一つにまとめられます。
const ranks = ["bronze","silver","gold"] as const;
type rankType = (typeof ranks)[number];
function isRank(rankStr:string):rankStr is rankType{
return ranks.includes(rankStr as rankType);
};
interface User {
name: string;
rank: rankType;
}
const users:User[] = [];
function addUser(name:string,rank:string):number{
if(!isRank(rank)){
console.log("Adding user failed.");
return -1;
}
return users.push({name,rank})-1;
}
このようにすると「ランク」の定義元が値として参照できるので、実行時にもしっかり動作します。また、ユニオン型を満たすかどうかは関数に定義しておくと再利用しやすく、より安全にもなりますね。
2. タグ付きユニオン型
例
type Member = {
userType: "member";
plan: "standard" | "family";
};
type Guest = {
userType: "guest";
loginType: string;
};
type Vip = {
userType: "vip";
grade: "silver" | "gold" | "platinum";
};
type User = Member | Guest | Vip;
function getPlan(user: User){
if(user.userType === "member"){
return user.plan;
}
if(user.userType === "vip"){
return user.grade;
}
return "free";
}
const userA = {
userType: "member",
plan: "standard"
};
getPlan(userA) // "standard"
上の例では、Userに3種類の型があり、それぞれ含まれるプロパティが違います。なので、引数そのままの状態でuserType以外のプロパティ(持っていない可能性のあるプロパティ)にアクセスするとコンパイルエラーとなります。
function getPlan(user: User){
user.plan//プロパティ 'plan' は型 'User' に存在しません。プロパティ 'plan' は型 'Guest' に存在しません。
そこで、これら構造を判別するためのプロパティ( タグ )を同じプロパティ名でそれぞれ用意しておくことにより、if文などでその値から型を 絞り込む ことができます。
まとめ
このように、TypeScriptではユニオン型という「いずれか」を表すことと、それらを絞り込むことで堅牢なコーディングをサポートしてくれます。まだまだ型システムは奥が深いのですが今回はここまでで。詳しくは、実際に取り組んでみたり、以下のものを見てみると少しずつ分かるかも。
TypeScriptで参考にしてるもの
書籍(部室にたぶんあるよ)
Webサイト