typeof型演算子とkeyof型演算子
ここからは、少しマニアックな内容です。型についての情報をあつかうTypeScriptの型演算子を2つ示します。typeof型演算子と、keyof型演算子です。
「typeof」「型演算子」、「keyof」「型演算子」です。単語の区切り位置に注意してください。また、typeof型演算子は、JavaScriptのtypeof演算子とは別物なので、混同しないようにしてください。
typeof型演算子は、変数から型を抽出します。例を示します。
let user = { name: "Tom", age: 10 }; type User = typeof user; let user2: User = { name: "Bob", age: 11 };
user
から型を得て、User
に型を代入して、user2: User
と利用しています。
このtype User = typeof user;
の部分はTypeScript独自の処理なので、npx tsc
でコンパイルすると消えます。npx tsc main.ts --target es2024
でコンパイルしたファイルは、次のようになります。あくまでも型をあつかう演算子なのが分かると思います。
let user = { name: "Tom", age: 10 }; let user2 = { name: "Bob", age: 11 };
続いて紹介するkeyof型演算子は、オブジェクトの型から、プロパティ名を型として返します。例を示します。
type Chara = { name: string; hp: number; mp: number; }; type CharaKey = keyof Chara;
型エイリアスCharaKey
には、"name" | "hp" | "mp"
が入ります。下の2つは同じ意味を持ちます。
type CharaKey = keyof Chara; type CharaKey = "name" | "hp" | "mp";
keyof型演算子は、プロパティ名の一覧の型を得ますので、プロパティ名のチェックに有用です。例を示します。
function getProfile(chara: Chara, prop: CharaKey): string { return `${chara.name}: ${chara[prop]}`; } let chara: Chara = { name: "Tom", hp: 10, mp: 5 }; console.log(getProfile(chara, "hp")); console.log(getProfile(chara, "mp")); console.log(getProfile(chara, "ap")); // エラーになる
CharaKey
を使って関数の引数をチェックします。最後の行の"ap"
は、関数の引数prop: CharaKey
には使えないのでエラーが出ます。
VSCodeで、この部分の赤線にマウスカーソルを重ねるとArgument of type '"ap"' is not assignable to parameter of type 'keyof Chara'.
("ap"' 型の引数は 'keyof Chara' 型のパラメータに割り当てることができません。)と表示されます。
自動でプロパティのユニオン型を作ってくれるのでメンテナンスが楽になります。
インターフェース
TypeScriptでは、クラスのインターフェースを実装できます。このインターフェースはTypeScriptの機能なので、コンパイルしてJavaScriptに出力すると消されるので注意が必要です。
TypeScriptのインターフェースの例を示します。JavaScriptのクラスのようにinterface
を書きます。クラスへの適用はimplements
を使います。また、複数適用して実装するには、,
で区切ります。
interface Chara { name: string; walk(): void; } interface StatMagick { cast(): void; } class Mage implements Chara, StatMagick { name: string = "魔術師"; walk(): void { console.log(`${this.name}、てくてく`); }; cast(): void { console.log(`${this.name}、どっかーん`); }; } let mage = new Mage(); mage.walk(); mage.cast();
implements Chara, StatMagick
の部分が、インターフェースを利用している部分になります。
また、インターフェースのフィールドにもreadonly
を付けられます。
interface User { readonly id: number; name: string; userType: string; }
インターフェースはextends
で継承できます。その際に、継承元のフィールドを宣言し直すこともできます。
interface Publisher extends User { userType: "Publisher"; item: string[]; }
また、TypeScriptのインターフェースには、少し変わった特徴があります。同じ名前のインターフェースを宣言するとマージされます。
下に示す1つ目のコードと、2つ目のコードは同じ意味になります。
interface CatAct { walk(): void; } interface CatAct { meow(): void; }
interface CatAct { walk(): void; meow(): void; }
TypeScriptと構造的型付け
プログラミング言語の世界では、型が同じか違うかを判別する方法は1つだけではありません。たとえばJavaでは、クラス名と継承構造を使って、同じか違うかを判断します。そしてTypeScriptでは、構造を見て、同じか違うかを判断します。
例を示します。
class Elf { talk(): void {}; }; class Human { talk(): void {} }; let elf: Elf = new Elf(); let human: Human = elf;
上のコードでは、クラスElf
のインスタンスを作り、型注釈としてHuman
を指定した変数human
に代入します。これはエラーが起きません。
Human
はtalk()
を持つ存在で、Elf
もtalk()
を持っているのでHuman
と見なせるからです。
こうしたプロパティは、完全に一致している必要はありません。条件を満たしていれば、不要なプロパティを持っていても構いません。例を示します。
class Elf { talk(): void {}; castMagick(): void {} }; class Human { talk(): void {} }; let elf: Elf = new Elf(); let human: Human = elf;
Elf
にはcastMagick()
というHuman
にはないプロパティがあります。しかし、Human
の条件であるtalk()
を持っているのでHuman
と見なせます。
3. まとめと次回予告
今回は、TypeScriptで型をあつかってきました。次回からは、さらに多様なデータに対応していきます。
- 第1回:【TypeScriptの基礎を学ぶ】JavaScriptと比較して起こりがちなミスを防ごう!
- 第2回:【TypeScriptの基礎を学ぶ】TypeScriptの型の付け方・あつかい方を解説
- 第3回:TypeScriptで多様なデータに対応方法(次回)
- 第4回:既存JavaScriptをTypeScriptに移行する方法
- 第5回:外部のパッケージやその他の情報