インターフェース (Interfaces)

TypeScriptでは型を表現する方法のひとつとしてインターフェースが存在します。

インターフェースを定義する

TypeScriptではinterface キーワードでインターフェースを定義できます。

interface Person {
name: string;
age: number;
}

TypeScriptのインターフェース

Javaなどのオブジェクト指向言語ではクラスの抽象的な型定義として利用されます。そのため、インターフェース単体では利用されず、特定のクラスがインターフェースを継承し実装を追加することで初めて効果を発揮します。

TypeScriptではインターフェースは型注釈として利用できるため、オブジェクトの型をInterfaceで定義するという使い方ができます。

interface Person {
name: string;
age: number;
}
const taro: Person = {
name: '太郎',
age: 12,
}

プロパティの宣言で使える便利な記号

タイプエイリアスと同じようにインターフェースの定義ではプロパティの宣言で選択可(Optional)、読み取り専用(Readonly)にすることができます。同様にインデックス型も使用可能です。こちらについては説明が重複しますのでタイプエイリアスのページをご参照ください。

継承 (Inheritance)

extendsキーワードを利用して定義済みのインターフェースを継承して新たにインターフェースを定義することができます。 インターフェースを継承した場合、継承元のプロパティの型情報はすべて引き継がれます。新しくプロパティを追加することもできますし、すでに宣言されているプロパティの型を部分型に指定しなおすこともできます。

プロパティを追加する

interface Person {
name: string;
age: number;
}
interface Student extends Person {
grade: number; // 学年
}
interface Teacher extends Person {
students: Student[]; // 生徒
}
const studentA: Student = {
name: '花子',
age: 10,
grade: 3,
}
const teacher: Teacher = {
name: '太郎',
age: 30,
students: [studentA],
}

プロパティを部分型に宣言しなおす

ある型からその型のリテラル型にすることも、ユニオン型から部分的に選択することもTypeScriptではそれぞれサブタイプにしていることと同じ意味があります。もちろん他のオブジェクト指向の言語と同じようにサブクラスにすることもできます。

リテラル型に変更する

interface WebPage {
path: string;
}
interface IndexPage extends WebPage {
path: '/';
}

ユニオン型から選ぶ

interface Person {
age: number | undefined;
}
interface Student extends Person {
age: number;
}

実装(Implementation)

他の言語と同じようにインターフェースをクラスが実装することもできます。実装時に複数のインターフェースを指定することもできます。そのときは,でインターフェースを区切り列挙します。このとき同じ名前のプロパティが違う型で衝突すると、その型はnever型になります。never型の変数には値の代入ができません。

interface Measurements {
bust: number;
waist: number;
hip: number;
}
interface SensitiveSizes {
bust: 'secret';
waist: 'secret';
hip: 'secret';
}
class Adorescent implements Measurements, SensitiveSizes {
// bust: never;
// waist: never;
// hip: never;
}

インターフェースが抱える問題

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

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

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

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

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

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

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

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

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

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