TypeScriptのinterfaceとtype:オブジェクトの型定義と使い分け
TypeScriptのインターフェースでオブジェクト、関数、クラスの契約を定義します。任意項目、readonly、extendsとimplements、構造的型付けの注意点も解説します。
インターフェースは、オブジェクトが持つプロパティやメソッドの型を定義する仕組みです。関数が必要とする値の形や、複数の実装に共通する契約を表せます。
この記事では、変数への指定、任意のプロパティ、構造による互換性、クラスでの実装、ファイル分割を確認します。TypeScript 4.9.4の strict 設定で確認した例です。インストール記事のNode.js環境を利用します。
インターフェースで契約を定義する
オブジェクトの形を定義する
src/interface-user.ts を作成します。
export {};
interface User {
readonly id: string;
name: string;
nickname?: string;
}
function getDisplayName(user: User): string {
return user.nickname ?? user.name;
}
const user: User = {
id: "user-1",
name: "葵",
};
console.log(getDisplayName(user)); // 葵
user.nickname = "あおい";
console.log(getDisplayName(user)); // あおい
id と name は必須、nickname は ? を付けた任意のプロパティです。読み取るときの nickname は string | undefined なので、ここでは ?? で未指定の場合に name を使っています。
readonly は、型チェック上でそのプロパティへの再代入を制限します。オブジェクトを実行時に凍結する処理ではありません。公式のプロパティ修飾子の説明
名前よりも必要な形が合うかを確認する
TypeScriptでは、基本的に値が必要な構造を持っているかを確認します。src/interface-structure.ts に保存します。
export {};
interface Named {
name: string;
}
function greet(person: Named): string {
return `こんにちは、${person.name}さん`;
}
const member = {
name: "蓮",
articleCount: 12,
};
console.log(greet(member)); // こんにちは、蓮さん
member は明示的に Named を宣言していませんが、必要な name: string を持つため渡せます。インターフェースは、追加のプロパティが一切存在しないことを常に保証する仕組みではありません。
一方、オブジェクトリテラルを直接代入・引数として渡す場面では、余分なプロパティの検査が行われ、名前の書き間違いなどを検出できます。変数経由の場合との違いを理解しておくと、診断の理由を追いやすくなります。公式の余剰プロパティ検査の説明
クラスの実装とファイル分割
次は二つのファイルを組み合わせます。src/speaker.ts に契約を定義します。
export interface Speaker {
readonly name: string;
speak(): string;
}
src/speaker-main.ts でその型を読み込み、クラスに実装します。
import type { Speaker } from "./speaker.js";
class Cat implements Speaker {
constructor(public readonly name: string) {}
speak(): string {
return `${this.name}: にゃー`;
}
}
function introduce(speaker: Speaker): void {
console.log(speaker.speak());
}
const cat = new Cat("ミケ");
introduce(cat); // ミケ: にゃー
const announcer: Speaker = {
name: "案内係",
speak() {
return `${this.name}: ようこそ`;
},
};
introduce(announcer); // 案内係: ようこそ
implements Speaker は、クラスのインスタンスが必要な形を満たすかを検査します。メソッドの実装を自動生成したり、別のクラスから処理を継承したりする機能ではありません。
Cat のようなクラスのインスタンスと、announcer のようなオブジェクトリテラルを、共通の Speaker として扱えます。型だけを読み込む import type は、JavaScript出力時には消去されます。
コンパイルして実行する
インストール記事の "type": "commonjs" と NodeNext の設定を前提に、ファイル分割の例は次のように実行できます。依存する src/speaker.ts もコンパイルされます。
npx tsc src/speaker-main.ts --strict --target ES2020 --module NodeNext --outDir dist --noEmitOnError
node dist/speaker-main.js
最初の二つの例も、入力と出力のファイル名を変更して実行できます。
インターフェースは実行時の検証ではない
APIから受け取った値へ User や Speaker と型を付けるだけでは、データの形を実行時に確認できません。インターフェースはJavaScriptに残らず、instanceof Speaker のような検査にも使えません。
外部入力は unknown として受け取り、必要なプロパティと値を検証する処理を用意します。インターフェースは、その検証後のコードで値を扱いやすくする役割があります。
typeとの使い分け
オブジェクトの形は type でも表現できます。interface は拡張や宣言のマージができ、type はユニオン型やタプルなどにも名前を付けられます。単純なオブジェクトの定義ではどちらも使えるため、表現したい型とプロジェクトの方針に合わせます。公式の型エイリアスとインターフェースの違い
インターフェースの設計で確認すること
関数に必要な形を小さく表す
大きなデータ型をどこでも要求するより、各関数が読む項目を明確にすると利用しやすくなります。オブジェクトが契約を満たすかを確認し、特定のクラスで作られたことだけに依存しない設計を検討します。
省略可能な項目を増やしすぎない
すべてを任意項目にすると、利用側で何度も存在確認が必要になります。状態によって必須項目が違うなら、判別可能なユニオンに分けると、不正な組み合わせを表しにくくできます。