メインコンテンツまでスキップ

インターフェースとinstanceof

インターフェースはTypeScriptで独自に定義された概念であり、JavaScriptには存在しません。つまりコンパイルをかけると消えてなくなります。そのため他の言語でできるようなその型が期待するインターフェースかどうかの判定ができません。上記のStudentインターフェースで次のようなことをしても実行することはできません。

typescript
if (studentA instanceof Student) {
// ...
}
// 'Student' only refers to a type, but is being used as a value here.
typescript
if (studentA instanceof Student) {
// ...
}
// 'Student' only refers to a type, but is being used as a value here.

これを解消するためには型ガードを自前で実装する必要があります。以下はその例のisStudent()です。

typescript
type UnknownObject<T extends object> = {
[P in keyof T]: unknown;
};
function isStudent(obj: unknown): obj is Student {
if (typeof obj !== "object") {
return false;
}
if (obj === null) {
return false;
}
const { name, age, grade } = obj as UnknownObject<Student>;
if (typeof name !== "string") {
return false;
}
if (typeof age !== "number") {
return false;
}
if (typeof grade !== "number") {
return false;
}
return true;
}
typescript
type UnknownObject<T extends object> = {
[P in keyof T]: unknown;
};
function isStudent(obj: unknown): obj is Student {
if (typeof obj !== "object") {
return false;
}
if (obj === null) {
return false;
}
const { name, age, grade } = obj as UnknownObject<Student>;
if (typeof name !== "string") {
return false;
}
if (typeof age !== "number") {
return false;
}
if (typeof grade !== "number") {
return false;
}
return true;
}

以下はisStudent()の解説です。

戻り値のobj is Student#

Type predicateと呼ばれる機能です。専門に解説してあるページがありますので参照ください。ここではこの関数が戻り値としてtrueを返すと、呼び出し元では引数objStudentとして解釈されるようになります。

UnknownObject#

typeofで判定されるobject型はオブジェクトではあるものの、プロパティが何も定義されていない状態です。そのためそのオブジェクトがどのようなプロパティを持っているかの検査すらできません。

typescript
const obj: object = {
name: "花子",
};
obj.name;
// Property 'name' does not exist on type 'object'.
typescript
const obj: object = {
name: "花子",
};
obj.name;
// Property 'name' does not exist on type 'object'.

そこでインデックス型を使っていったんオブジェクトのいかなるプロパティもunknown型のオブジェクトであると型アサーションを使い解釈させます。これですべてのstring型のプロパティにアクセスできるようになります。あとは各々のunknown型のプロパティをtypeof, instanceofで判定させればこの関数の判定が正しい限りTypeScriptは引数が期待するStudentインターフェースを実装したオブジェクトであると解釈します。

関数の判定が正しい限りとは#

インターフェースに変更が加わった時この関数も同時に更新されないとこの関係は崩れてしまいます。たとえばstudent.nameは現在string型ですが、これが姓名の区別のために次のようなオブジェクトに差し替えられたとします。

typescript
interface Name {
surname: string;
givenName: string;
}
typescript
interface Name {
surname: string;
givenName: string;
}

この変更に対しisStudent()も随伴して更新されなければこの関数がStudentインターフェースであると判定するオブジェクトのnameプロパティは明らかに違うものになるでしょう。