Menu

TypeScriptのinterface: 書き方、省略可能、メソッドと例

interface はオブジェクトの形、つまりどんなプロパティを持ち、それぞれがどんな型かに名前を付けます。宣言の書き方、省略可能なプロパティと readonly プロパティ、メソッド、インデックスシグネチャ、継承、クラスでの実装、宣言のマージ、ジェネリックなインターフェース、インターフェースにデフォルト値を与える方法を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

インターフェースは、オブジェクトの形、つまり持たなければならないプロパティとそれぞれの型に名前を付けます。宣言したあとはその名前を型として使い、コンパイラーは渡す、返す、代入するすべてのオブジェクトをそれと照らし合わせてチェックします。

最後の呼び出しはコンパイルエラー TS2741 です。インターフェースはコードのコンパイル時に消去されます。JavaScriptの出力には User の痕跡はなく、実行時に形をチェックするものは何もありません。

インターフェースの宣言

構文は、キーワード interface、名前(慣習では PascalCase)、そしてメンバーを並べた本体です。メンバーはセミコロン、カンマ、または改行だけで区切れます。一般的なスタイルはセミコロンです。

interface Product {
  sku: string;               // required property
  price: number;
  tags: string[];            // array property
  dimensions: {              // nested object type
    width: number;
    height: number;
  };
  discount?: number;         // optional property
  readonly createdAt: Date;  // cannot be reassigned
  label(): string;           // method
}

インターフェースは型であって値ではありません。new でインスタンス化できず、デフォルト値もなく、obj instanceof Product はエラー TS2693('Product' only refers to a type, but is being used as a value here)です。実行時に形をチェックするには 型ガード を書きます。

構造的型付けと余分なプロパティのチェック

TypeScriptは名前ではなく形を比較します。必要なプロパティを持つオブジェクトなら、そのインターフェースで宣言されたかどうかにかかわらず当てはまります。余分なプロパティがあってもかまいませんが、1つだけ例外があります。インターフェースが期待される場所に直接書いたオブジェクトリテラルには、余分なプロパティのチェックが行われます。そこでの知らないキーはほとんどの場合打ち間違いだからです。

このエラー(TS2353)が、User に対する { id: 1, name: "a", emial: "x" } を捕まえます。コンパイラーは Did you mean to write 'email'?(TS2561)と提案までしてくれます。

省略可能なプロパティと readonly プロパティ

名前のあとの ? はプロパティを省略可能にします。オブジェクトはそれを省けて、読むと T | undefined になります。readonly は、オブジェクトを作ったあとのプロパティの再代入を禁止します。

ここには2つの限界が表れています。1つ目に、readonly はコンパイル時だけのものです。@ts-expect-error を付けた2行は、Run を押すと実行されて成功しますし、最後の数行は readonly のない型の参照を通して apiUrl を変更しています。2つ目に、readonly は浅いものです。readonly hosts: string[] なら hosts の再代入は止められますが hosts.push(...) はできてしまうので、配列自体を readonly string[] と型付けしています。readonly は型付きのコードの中で意図を文書化して強制するもので、何も凍結しません。ユーティリティ型 Readonly<T> を使うと、既存のインターフェースのすべてのプロパティを一度に readonly にできます。

メソッドと関数のプロパティ

メソッドは、メソッドシグネチャ name(params): ReturnType としても、関数を持つプロパティ name: (params) => ReturnType としても書けます。呼び出し側はどちらも同じように使います。

違いは微妙です。strictFunctionTypes(strict に含まれます)のもとでは、関数型のプロパティの引数は厳密にチェックされますが、メソッドシグネチャはゆるく(双変で)チェックされるので、プロパティの形のほうが少し多くの間違いを捕まえます。メソッド構文は短く、より一般的なスタイルです。どちらでも問題ありません。

インターフェースは、呼び出しシグネチャやコンストラクトシグネチャを使って、呼び出せるものやインスタンスを作れるものも表せます:

interface Formatter {
  (value: number): string; // call signature: the object is a function
  locale: string;          // and it also has a property
}

interface PointConstructor {
  new (x: number, y: number): { x: number; y: number }; // construct signature
}

インデックスシグネチャ

プロパティ名が前もってわからないときは、インデックスシグネチャでそれらをまとめて表します。[key: string]: T は「任意の文字列のキーで、それぞれが T を持つ」という意味です。

最後の数行が落とし穴を示しています。存在しないキーを読むと、型は number | undefined ではなく number になります。コンパイラーのオプション noUncheckedIndexedAccess を使うと、そうした読み取りすべてに | undefined が加わります。

名前付きのプロパティをインデックスシグネチャと並べることはできますが、インデックスシグネチャに合っていなければなりません。interface Dict { [key: string]: number; name: string } はエラー TS2411 Property 'name' of type 'string' is not assignable to 'string' index type 'number' です。インデックスの型を広げる([key: string]: number | string)か、動的な部分を独自のプロパティに移しましょう。単純なキーと値の対応なら、Record<string, number> が同じことを1行で表します。

インターフェースの継承

extends は、既存のインターフェース1つ以上から新しいインターフェースを作ります。子は親のすべてのメンバーと自分のメンバーを持ちます:

interface Animal {
  name: string;
}

interface Pet extends Animal {
  owner: string;
}

interface Trained {
  commands: string[];
}

interface ServiceDog extends Pet, Trained {
  certifiedUntil: Date;
}

// ServiceDog requires: name, owner, commands, certifiedUntil

子が親のプロパティを再宣言できるのは、互換性のある(より狭い)型の場合だけです。たとえば親が kind: string のところを kind: "dog" にする場合です。ルールと型エイリアスの拡張方法は extends のページにあります。

クラスでインターフェースを実装する

class X implements Shape は、インターフェースが要求するものをクラスがすべて持っているかのチェックをコンパイラーに頼みます。メンバーが足りなければ、クラスの宣言の位置でエラーになります:

index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
  Property 'area' is missing in type 'Circle' but required in type 'Shape'.

area() を追加すれば、いくつものクラスや普通のオブジェクトさえも、すべて Shape として使えます:

implements はチェックにすぎません。クラスにメンバーを追加せず、クラスのメソッドの引数に型を付けてもくれません。greet(name: string): string を実装するクラスの中の greet(name) {} は、やはりエラー TS7006 Parameter 'name' implicitly has an 'any' type です。クラスは複数のインターフェースを実装できます: class A implements B, C。

宣言のマージ

同じスコープで同じ名前のインターフェースを2回宣言すると、2つは1つにマージされます。これは型エイリアスにはできないことです(同じ名前の2つ目の type は識別子の重複エラーになります)。

interface Settings {
  theme: string;
}

interface Settings {
  fontSize: number;
}

// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };

アプリケーションのコードでこれが望ましいことはめったになく、意図しないマージは混乱のもとです。本当の用途は、自分が管理していない型にメンバーを追加することです。ライブラリのオプションや、Window のようなグローバルなどです。モジュールの中からは、宣言を declare global で囲みます:

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}

export {};

こうすると、プロジェクトのどこでも window.analytics.track("signup") が型チェックを通ります。型定義パッケージも同じ仕組みに頼っています。型定義ファイル を参照してください。

インターフェースのプロパティのデフォルト値

インターフェースは型を表し、実行時には消去されるので、デフォルト値を持てません。size?: "sm" | "md" = "md" はエラー TS1246 An interface property cannot have an initializer です。プロパティを省略可能にし、オブジェクトを使う場所でデフォルト値を埋めましょう:

分割代入のデフォルト値のほうが安全です。明示的な size: undefined を含め、値が undefined のときにはいつでも適用されます。スプレッドの方法では明示的な undefined がデフォルト値の上にコピーされ、しかも結果は size が常に設定されているかのように型付けされます。型でその保証が必要なら、exactOptionalPropertyTypes を有効にしましょう。省略可能な size?: ... に対する size: undefined がコンパイルエラーになります。オブジェクトに振る舞いも必要なら、初期値付きのフィールドを持つクラスがもう1つの選択肢です。

ジェネリックなインターフェース

インターフェースは型パラメーターを取れるので、1つの宣言でさまざまなデータの型に対応できます:

interface ApiResponse<T> {
  ok: boolean;
  data: T;
  error?: string;
}

interface Page<T> {
  items: T[];
  nextCursor?: string;
}

interface User {
  id: number;
  name: string;
}

const res: ApiResponse<Page<User>> = {
  ok: true,
  data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};

ApiResponse<Page<User>> は「データがユーザーのページであるレスポンス」と読めます。標準ライブラリにはこうしたものがたくさんあります。Array<T>、Promise<T>、Map<K, V> はどれもジェネリックなインターフェースです。

インターフェースと型エイリアス

type エイリアスでも同じオブジェクトの形を表せ、普通のオブジェクト型なら2つは交換可能です。マージできるのはインターフェースだけで、ユニオン型、タプル、マップ型、条件型に名前を付けられるのは型エイリアスだけです。TypeScriptのハンドブックの目安は、type にしかない機能が必要になるまでは interface を使うことです。多くの人が驚く Record<string, ...> との違いを含め、完全な比較は interface と type のページにあります。

よくある質問

TypeScriptの interface とは何ですか?

interface は、オブジェクトの形に名前を付けた記述です。プロパティ名とその型、どれが省略可能か readonly か、そしてメソッドを表します。コンパイラーは、そのインターフェースとして使われる値がその形を持っているかをチェックします。インターフェースはコンパイル時にしか存在せず、JavaScriptは何も生成しません。

TypeScriptの interface にデフォルト値を設定するには?

できません。インターフェースは値ではなく型を表すので、size: "md" = ... は正しい構文ではありません。プロパティを省略可能にし(size?: "sm" | "md")、オブジェクトを使う場所でデフォルト値を適用します。ふつうは関数の引数での分割代入のデフォルト値を使います: function render({ size = "md" }: Options)。デフォルトのオブジェクトをスプレッドする方法({ ...DEFAULTS, ...options })も使えますが、options の中の明示的な undefined がデフォルト値を上書きしてしまいます。

オブジェクトがインターフェースを実装しているかを実行時にチェックするには?

インターフェースはコンパイル時に消去されるので、組み込みの方法はありません。obj instanceof User はエラー TS2693('User' only refers to a type, but is being used as a value here)です。プロパティをチェックする型ガード関数 function isUser(x: unknown): x is User { ... } を書くか、スキーマライブラリで検証しましょう。

interface は複数のインターフェースを継承できますか?

できます。extends のあとにカンマ区切りで並べます: interface ServiceDog extends Pet, Trained { ... }。新しいインターフェースは各親のすべてのメンバーと自分のメンバーを持ちます。2つの親が同じプロパティを互換性のない型で宣言していると、宣言がエラーになります。

TypeScriptのインターフェースとクラスの違いは何ですか?

クラスは実行時に存在し、コンストラクターとメソッドの実装を持ち、new でオブジェクトを作れます。インターフェースはコンパイラーのために形を表すだけで、JavaScriptの出力からは消去されます。クラスは implements SomeInterface と宣言すると一致しているかをコンパイラーにチェックさせられ、正しい形を持つ普通のオブジェクトもそのインターフェースに当てはまります。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める