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

interfaceとtypeの違い

型エイリアスを利用することで、インターフェースと同様の定義が行なえます。

ts
interface Animal {
name: string;
bark(): string;
}
type Animal = {
name: string;
bark(): string;
};
ts
interface Animal {
name: string;
bark(): string;
}
type Animal = {
name: string;
bark(): string;
};

この章では、インターフェースと型エイリアスの違いについて詳しく説明していきます。

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

内容インターフェース型エイリアス
継承可能不可。ただし交差型で表現は可能
継承による上書き上書きまたはエラーフィールド毎に交差型が計算される
同名のものを宣言定義がマージされるエラー
Mapped Types使用不可使用可能
インデックス型への代入互換性なしあり

継承

インターフェースは、インターフェースや型エイリアスを継承できます。

ts
interface Animal {
name: string;
}
type Creature = {
dna: string;
};
interface Dog extends Animal, Creature {
dogType: string;
}
ts
interface Animal {
name: string;
}
type Creature = {
dna: string;
};
interface Dog extends Animal, Creature {
dogType: string;
}

一方、型エイリアスは継承は行えません。代わりに交差型(&)を使用することで、継承と似たことを実現できます。

ts
type Animal = {
name: string;
};
type Creature = {
dna: string;
};
type Dog = Animal &
Creature & {
dogType: string;
};
ts
type Animal = {
name: string;
};
type Creature = {
dna: string;
};
type Dog = Animal &
Creature & {
dogType: string;
};

プロパティのオーバーライド

インターフェースで継承の際にプロパティをオーバーライドすると、継承元のプロパティの型が上書きされます。

ts
// OK
interface Animal {
name: any;
price: {
yen: number;
};
legCount: number;
}
 
interface Dog extends Animal {
name: string;
price: {
yen: number;
dollar: number;
};
}
 
// 最終的なDogの定義
interface Dog {
name: string;
price: {
yen: number;
dollar: number;
};
legCount: number;
}
ts
// OK
interface Animal {
name: any;
price: {
yen: number;
};
legCount: number;
}
 
interface Dog extends Animal {
name: string;
price: {
yen: number;
dollar: number;
};
}
 
// 最終的なDogの定義
interface Dog {
name: string;
price: {
yen: number;
dollar: number;
};
legCount: number;
}

ただし、オーバーライドするためには元の型に代入できるものでなければなりません。次の例はnumber型であるフィールドをstring型でオーバーライドしようとしている例です。

ts
interface A {
numberField: number;
price: {
yen: number;
dollar: number;
};
}
 
interface B extends A {
Interface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.2430Interface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.
numberField: string;
price: {
yen: number;
euro: number;
};
}
ts
interface A {
numberField: number;
price: {
yen: number;
dollar: number;
};
}
 
interface B extends A {
Interface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.2430Interface 'B' incorrectly extends interface 'A'. Types of property 'numberField' are incompatible. Type 'string' is not assignable to type 'number'.
numberField: string;
price: {
yen: number;
euro: number;
};
}

一方、型エイリアスの場合は上書きにはならず、フィールドの型の交差型が計算されます。また、交差型で矛盾があって計算できない場合もコンパイルエラーにはなりません。

ts
type Animal = {
name: number;
price: {
yen: number;
dollar: number;
};
};
 
type Dog = Animal & {
name: string;
price: {
yen: number;
euro: number;
};
};
 
// 最終的なDogの定義
type Dog = {
name: never; // 交差型を作れない場合はコンパイルエラーではなくnever型になる
price: {
yen: number;
dollar: number;
euro: number;
};
};
ts
type Animal = {
name: number;
price: {
yen: number;
dollar: number;
};
};
 
type Dog = Animal & {
name: string;
price: {
yen: number;
euro: number;
};
};
 
// 最終的なDogの定義
type Dog = {
name: never; // 交差型を作れない場合はコンパイルエラーではなくnever型になる
price: {
yen: number;
dollar: number;
euro: number;
};
};

同名のものを宣言

型エイリアスは同名のものを複数定義できず、コンパイルエラーになります。

ts
type SameNameTypeWillError = {
Duplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.
message: string;
};
type SameNameTypeWillError = {
Duplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.
detail: string;
};
ts
type SameNameTypeWillError = {
Duplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.
message: string;
};
type SameNameTypeWillError = {
Duplicate identifier 'SameNameTypeWillError'.2300Duplicate identifier 'SameNameTypeWillError'.
detail: string;
};

一方、インターフェースの場合は、同名のインターフェースを定義でき、同名の定義をすべて合成したインターフェースになります。
ただし、同名のフィールドだが、型の定義が違っている場合はコンパイルエラーになります。

ts
interface SameNameInterfaceIsAllowed {
myField: string;
sameNameSameTypeIsAllowed: number;
sameNameDifferentTypeIsNotAllowed: string;
}
 
interface SameNameInterfaceIsAllowed {
newField: string;
sameNameSameTypeIsAllowed: number;
}
 
interface SameNameInterfaceIsAllowed {
sameNameDifferentTypeIsNotAllowed: number;
Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.2717Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.
}
ts
interface SameNameInterfaceIsAllowed {
myField: string;
sameNameSameTypeIsAllowed: number;
sameNameDifferentTypeIsNotAllowed: string;
}
 
interface SameNameInterfaceIsAllowed {
newField: string;
sameNameSameTypeIsAllowed: number;
}
 
interface SameNameInterfaceIsAllowed {
sameNameDifferentTypeIsNotAllowed: number;
Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.2717Subsequent property declarations must have the same type. Property 'sameNameDifferentTypeIsNotAllowed' must be of type 'string', but here has type 'number'.
}

Mapped Types

Mapped Typesについては別のページで詳しく説明しますので、ここでは型エイリアスとインターフェースのどちらで使えるかだけを説明します。

📄️ Mapped Types

インデックス型では設定時はどのようなキーも自由に設定できてしまい、アクセス時は毎回undefinedかどうかの型チェックが必要です。入力の形式が決まっているのであればMapped Typesの使用を検討できます。

Mapped Typesは型のキーを動的に指定することができる仕組みであり、型エイリアスでのみ利用することができます。
次の例ではユニオン型の一覧をキーとした新しい型を生成しています。

typescript
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
type Butterfly = {
[key in SystemSupportLanguage]: string;
};
typescript
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
type Butterfly = {
[key in SystemSupportLanguage]: string;
};

インターフェースでMapped Typesを使うとエラーになります。

typescript
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
 
interface Butterfly {
[key in SystemSupportLanguage]: string;
A mapped type may not declare properties or methods.7061A mapped type may not declare properties or methods.
}
typescript
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
 
interface Butterfly {
[key in SystemSupportLanguage]: string;
A mapped type may not declare properties or methods.7061A mapped type may not declare properties or methods.
}

インデックス型への代入互換性

インターフェースと型エイリアスでは、インデックス型への代入互換性が異なります。

  • 型エイリアス: インデックス型に代入できる
  • インターフェース: インデックス型に代入できない

次の例では、同じ構造を持つインターフェースと型エイリアスを、インデックス型に代入しています。

ts
interface PersonInterface {
name: string;
age: string;
}
 
type PersonType = {
name: string;
age: string;
};
 
const personInterface: PersonInterface = { name: "Alice", age: "25" };
const personType: PersonType = { name: "Bob", age: "30" };
 
// インターフェースはインデックス型に代入できない
const stringMap1: { [K: string]: string } = personInterface;
Type 'PersonInterface' is not assignable to type '{ [K: string]: string; }'. Index signature for type 'string' is missing in type 'PersonInterface'.2322Type 'PersonInterface' is not assignable to type '{ [K: string]: string; }'. Index signature for type 'string' is missing in type 'PersonInterface'.
 
// 型エイリアスはインデックス型に代入できる
const stringMap2: { [K: string]: string } = personType; // OK
ts
interface PersonInterface {
name: string;
age: string;
}
 
type PersonType = {
name: string;
age: string;
};
 
const personInterface: PersonInterface = { name: "Alice", age: "25" };
const personType: PersonType = { name: "Bob", age: "30" };
 
// インターフェースはインデックス型に代入できない
const stringMap1: { [K: string]: string } = personInterface;
Type 'PersonInterface' is not assignable to type '{ [K: string]: string; }'. Index signature for type 'string' is missing in type 'PersonInterface'.2322Type 'PersonInterface' is not assignable to type '{ [K: string]: string; }'. Index signature for type 'string' is missing in type 'PersonInterface'.
 
// 型エイリアスはインデックス型に代入できる
const stringMap2: { [K: string]: string } = personType; // OK

実際のユースケース

この違いは、Record<string, T>のようなユーティリティ型を使う場面でよく遭遇します。

ts
interface User {
name: string;
email: string;
}
 
function printUser(user: Record<string, string>) {
console.log(user);
}
 
const user: User = { name: "alice", email: "alice@example.com" };
printUser(user); // エラー
Argument of type 'User' is not assignable to parameter of type 'Record<string, string>'. Index signature for type 'string' is missing in type 'User'.2345Argument of type 'User' is not assignable to parameter of type 'Record<string, string>'. Index signature for type 'string' is missing in type 'User'.
ts
interface User {
name: string;
email: string;
}
 
function printUser(user: Record<string, string>) {
console.log(user);
}
 
const user: User = { name: "alice", email: "alice@example.com" };
printUser(user); // エラー
Argument of type 'User' is not assignable to parameter of type 'Record<string, string>'. Index signature for type 'string' is missing in type 'User'.2345Argument of type 'User' is not assignable to parameter of type 'Record<string, string>'. Index signature for type 'string' is missing in type 'User'.

このような場合、型エイリアスで定義するか、後述の回避策を使う必要があります。

回避策

インターフェースをインデックス型に代入したい場合、いくつかの回避策があります。

値レベルの回避策: スプレッド構文を使う

スプレッド構文を使うと、インターフェースの型情報が再計算され、代入できるようになります。

ts
const stringMap: { [K: string]: string } = { ...personInterface }; // OK
ts
const stringMap: { [K: string]: string } = { ...personInterface }; // OK
型レベルの回避策: Pickを使う

Pickを使うと、インターフェースから型エイリアス相当の型を作成できます。

ts
type PersonType = Pick<PersonInterface, keyof PersonInterface>;
type Test = OnlyStringValues<PersonType>; // OK
ts
type PersonType = Pick<PersonInterface, keyof PersonInterface>;
type Test = OnlyStringValues<PersonType>; // OK
インターフェースにインデックス型を明示的に追加する

インターフェースの定義を変更できる場合は、インデックス型を明示的に追加する方法もあります。

ts
interface PersonInterface {
name: string;
age: string;
[K: string]: string; // 明示的にインデックス型を追加
}
 
const person: PersonInterface = { name: "Alice", age: "25" };
const stringMap: { [K: string]: string } = person; // OK
ts
interface PersonInterface {
name: string;
age: string;
[K: string]: string; // 明示的にインデックス型を追加
}
 
const person: PersonInterface = { name: "Alice", age: "25" };
const stringMap: { [K: string]: string } = person; // OK

📄️ インデックス型

TypeScriptで、オブジェクトのフィールド名をあえて指定せず、プロパティのみを指定したい場合があります。そのときに使えるのがこのインデックス型(index signature)です。たとえば、プロパティがすべてnumber型であるオブジェクトは次のように型注釈します。

インターフェースと型エイリアスの使い分け

実際に型を定義する時にインターフェースと型エイリアスのどちらを使うのがよいのでしょうか?残念ながら、これに関しては明確な正解はありません。

インターフェースと型エイリアスのどちらでも型を定義することができますが、拡張性やMapped Typesの利用可否といった点で異なる部分が存在するので、これらのメリット・デメリットを考慮してプロジェクト内でルールを決めてそれに遵守するようにしましょう。

参考例として、Googleが公開しているTypeScriptのスタイルガイドの型エイリアスvsインターフェースの項目では、プリミティブな値やユニオン型やタプルの型定義をする場合は型エイリアスを利用し、オブジェクトの型を定義する場合はインターフェースを使うことを推奨しています。

インターフェースと型エイリアスの使い分けに悩む場面が多く開発スピードが落ちてしまうのであれば、型エイリアスに統一して書く方針にする考え方もあります。

インターフェースの利用例

ライブラリを作成する際に定義した型の構造がアプリケーション側に依存するような場合にはインターフェースを利用するのが適切です。

Node.jsのprocess.envの型定義は@types/node/process.d.tsで次のように実装されています。

ts
declare module "process" {
global {
namespace NodeJS {
interface ProcessEnv extends Dict<string> {
TZ?: string;
}
}
}
}
ts
declare module "process" {
global {
namespace NodeJS {
interface ProcessEnv extends Dict<string> {
TZ?: string;
}
}
}
}

インターフェースで型定義されていることで、パッケージを利用する側で型の拡張が自由に行えるようになっています。

もしProcessEnvが型エイリアスで定義されていると型の拡張が行えず、とても開発しづらい状態になってしまいます。このように不特定多数のユーザーが型を参照するような場合には、拡張性を考慮してインターフェースで型定義をするようにしましょう。

ts
// src/types/global.d.ts
declare module "process" {
global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production";
}
}
}
}
ts
// src/types/global.d.ts
declare module "process" {
global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production";
}
}
}
}

関連情報

📄️ インターフェース

インターフェースはクラスが実装すべきフィールドやメソッドを定義した型です。クラスはインターフェースを実装することで、インターフェースが求めるメソッド名や引数の型に則っているかをチェックすることができます。

📄️ 型エイリアス

TypeScriptでは、型に名前をつけられます。名前のついた型を型エイリアス(タイプエイリアス; type alias)と呼びます。