フロントエンド開発では、ずっと type を使ってきた。ところがAIにコードを書かせると、やたらと interface を多用してくる。困惑したので、改めて両者の違いを整理しておく。白状すると、僕の認識はいささか心許ない。関わってきたNext.jsやらReactやらのプロジェクトで、みんなが type を使っていたから使っていた。それだけの話だったのである。 先に、とりあえずの結論を書いておく。理由がなければ type を使う。クラスを使う場合や、外部から拡張する必要がある場合は interface を使う。そして、なによりもプロジェクトの方針に従う。

とりあえずの結論

理由がなければ type でよい。そう考える理由は、ユニオン型や交差型、タプル型、条件付き型といった「型計算」を扱いやすいからだ。ReactのProps定義やユーティリティ型との相性もいい。普通の業務アプリケーション開発なら、パフォーマンス上の問題もまず起きない。可読性と柔軟性の観点からも、type を基準にするのが自然なのである。 一方、クラスを使うOOPスタイルの設計や、外部からの拡張が必要な場面では interface を使う。interface はクラスの implements に馴染みやすく、OOPで書くならこちらの方が直感的だ。しかも宣言マージやモジュール拡張ができる。Expressの Request にフィールドを足したり、styled-componentsの DefaultTheme を拡張したりと、外部ライブラリの型を広げたいなら interface 一択になる。 もっとも、最優先すべきはプロジェクトの方針である。Googleのような大規模組織では、「オブジェクト型は interface」と明確に決めていることがある。チーム開発で効いてくるのは、どちらを使うかより、一貫して使っているかどうかだ。その一貫性こそが、コードベース全体の可読性と保守性を底上げするのである。

基本の定義

そもそも、どちらも同じようにオブジェクトの型を定義できる。interface なら次のように書く。
interface User {
  id: number;
  name: string;
  email: string;
}
type なら次のようになる。
type User = {
  id: number;
  name: string;
  email: string;
};

見た目はほとんど変わらない。違いは細部に潜んでいる。神は細部に宿るというが、型の場合は罠も一緒に宿っているから困る。

宣言のマージ

まず、interface は同じ名前で複数回宣言すると、自動的にマージされる。
interface User {
  id: number;
  name: string;
}

interface User {
  email: string;
}

// 結果的に以下と同じになる
interface User {
  id: number;
  name: string;
  email: string;
}
正直なところ、この仕様は少々物騒である。どこか遠くのファイルで同名の interface を宣言されたら、知らないうちにプロパティが生えてくるのだ。対して type は、同じ名前で複数回宣言するとエラーになる。
type User = {
  id: number;
  name: string;
};

// エラー: Duplicate identifier 'User'
type User = {
  email: string;
};

継承と交差型

次に継承の書き方である。interface は extends で他の interface を継承でき、type からも継承できる。
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// type からも継承可能
type Cat = {
  meow: () => void;
};

interface PersianCat extends Cat {
  furColor: string;
}
type には extends キーワードがないが、交差型(&)を使えば同じようなことができる。
type Animal = {
  name: string;
};

type Dog = Animal & {
  breed: string;
};
ただし、継承時にプロパティを上書きしようとすると、両者の振る舞いが分かれる。interface の場合は、継承先でより具体的な型に変更できる。
interface Animal {
  name: string | number;
}

interface Dog extends Animal {
  name: string; // より具体的な型に変更可能
}
ところが type の交差型では、上書きではなく型の積になる。string & (string | number) は string なので、一見うまくいく。しかし矛盾する型を指定すると、never になってしまう。
type Animal = {
  name: string | number;
};

type Dog = Animal & {
  name: string; // 交差型により string & (string | number) = string
};

// しかし、矛盾する型を指定すると never になる
type Cat = Animal & {
  name: boolean; // boolean & (string | number) = never
};
とはいえ、Genericsを使えば type でも素直に上書きできる。いったん Omit で該当キーを取り除き、それから交差させればよい。
type Override<T, U> = Omit<T, keyof U> & U;

type Animal = {
  name: string | number;
};

type Dog = Override<Animal, { name: string }>;
// => { name: string }  // 期待どおり上書き

type Cat = Override<Animal, { name: boolean }>;
// => { name: boolean } // neverにならず、素直に boolean に上書き

typeにしかできないこと

翻って、type にしかできないことも多い。代表的なのが、ユニオン型やプリミティブ型のエイリアスである。interface でこれを書こうとするとエラーになる。
// type では可能
type Status = "loading" | "success" | "error";
type ID = string | number;
type UserId = string;

// interface では不可能
// interface Status = "loading" | "success" | "error"; // エラー
条件型やマップ型のような高度な型操作も type の領分だ。
// 条件型
type NonNullable<T> = T extends null | undefined ? never : T;

// マップ型
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// interface ではこれらは不可能
マップ型についてはもう一例挙げておく。Readonly や Partial のようなユーティリティ型は、まさにこの仕組みで作られている。
// type でのみ可能
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type Partial<T> = {
  [P in keyof T]?: T[P];
};

// interface では不可能
// interface ReadonlyInterface<T> = {
//   readonly [P in keyof T]: T[P]; // エラー
// };
タプル型も、素直に定義できるのは type だけだ。interface でもインデックスと length を並べれば似たものは作れる。ただ、これを好んで書く奇特な人はまずいまい。
// type では可能
type Coordinates = [number, number];
type RGB = [number, number, number];

// interface では直接的には不可能(配列として定義は可能)
interface CoordinatesInterface {
  0: number;
  1: number;
  length: 2;
}

Googleスタイルガイドの立場

では、業界ではどう使い分けているのか。Googleのスタイルガイドは、プリミティブ型・ユニオン型・タプル型には type を推奨している。オブジェクトの型定義には interface だ。理由として挙げられているのは次のくだりである。

These forms are nearly equivalent, so under the principle of just choosing one out of two forms to prevent variation, we should choose one. Additionally, there are also interesting technical reasons to prefer interface. That page quotes the TypeScript team lead: Honestly, my take is that it should really just be interfaces for anything that they can model. There is no benefit to type aliases when there are so many issues around display/perf. https://google.github.io/styleguide/tsguide.html#prefer-interfaces

https://ncjamieson.com/prefer-interfaces/

ほぼ等価なのだから、揺れを防ぐためにどちらか一方に決めよう。そのうえで、技術的には interface を推す理由もある。そういう主張だ。この方針に従うと、たとえば次のような書き分けになる。
// type を使う例
type Status = "loading" | "success" | "error";
type Point = [number, number];
type ID = string;

// interface を使う例
interface User {
  id: ID;
  name: string;
  status: Status;
}
この方針に沿って整理すると、interface を使うのは、まずオブジェクトの形状を定義する場合だ。
interface ApiResponse {
  data: any;
  status: number;
  message: string;
}
次に、ライブラリやAPIの型定義で拡張の余地を残したい場合である。ライブラリ側で宣言した interface を、利用者側で同名の宣言によって広げられる。
// ライブラリ側
interface Config {
  apiUrl: string;
}

// ユーザー側で拡張
interface Config {
  timeout: number;
}
そしてクラスに実装させる場合も interface が馴染む。
interface Drawable {
  draw(): void;
}

class Circle implements Drawable {
  draw() {
    // 実装
  }
}
逆に type を使うのは、ユニオン型や関数型などのエイリアスを作る場合だ。
type Theme = "light" | "dark";
type EventHandler = (event: Event) => void;
条件型やマップ型のような複雑な型操作をする場合も type になる。
type Partial<T> = {
  [P in keyof T]?: T[P];
};

タプル型も同様である。

type Point = [number, number];
最後に、交差型で既存の型に項目を足して合成する場合も type の出番だ。
type UserWithTimestamps = User & {
  createdAt: Date;
  updatedAt: Date;
};

コンパイラの中の扱い

ここからは、TypeScriptコンパイラの中を覗いてみる。そもそもコンパイラは、interface と type を別々のやり方で処理している。interface は名前付きのシンボルとして、シンボルテーブルに登録される。型チェックでは、その名前で参照されるわけだ。
interface User {
  name: string;
  age: number;
}

// コンパイラ内部では名前付きシンボルとして保存
// - シンボルテーブルに "User" として登録
// - 型チェック時に名前で参照
// - エラーメッセージでは型名が表示される
片や type は型エイリアスとして処理され、必要に応じて構造が展開される。複雑な型だと、完全に展開されてしまうこともある。
type User = {
  name: string;
  age: number;
};

// コンパイラ内部では構造的に展開される
// - 型エイリアスとして処理
// - 必要に応じて型が展開される
// - 複雑な型の場合は完全に展開されることがある
この違いは、型チェックのパフォーマンスにも表れる。interface の継承チェーンは効率的に処理される。各 interface が、独立したシンボルとして管理されているからだ。
interface BaseConfig {
  apiUrl: string;
}

interface UserConfig extends BaseConfig {
  userId: string;
}

interface AdminConfig extends BaseConfig {
  adminKey: string;
}

// 継承チェーンが効率的に処理される
// 各interfaceは独立したシンボルとして管理
一方、type で同じ構造を作ると、交差型の計算が必要になる。使うたびに、型の合成処理が走るのだ。
type BaseConfig = {
  apiUrl: string;
};

type UserConfig = BaseConfig & {
  userId: string;
};

type AdminConfig = BaseConfig & {
  adminKey: string;
};

// 交差型の計算が必要
// 使用時に型の合成処理が発生
エラーメッセージにも差が出る。単純なオブジェクト型なら、interface でも type でも型名がそのまま表示される。
interface User {
  name: string;
  age: number;
}

const user: User = { name: "Alice" };
// Error: Property 'age' is missing in type '{ name: string; }'
//        but required in type 'User'.
ところが type で交差型を重ねると、型名ではなく、展開された構造がずらずらと並ぶ。三つ程度ならまだ読める。十も重なれば、読む気が雲散霧消する代物と化す。
type User = {
  name: string;
  age: number;
};

const user: User = { name: "Alice" };
// Error: Property 'age' is missing in type '{ name: string; }'
//        but required in type 'User'.

// 複雑な型の場合
type ComplexUser = {
  name: string;
} & {
  age: number;
} & {
  email: string;
};

const complexUser: ComplexUser = { name: "Alice" };
// Error: Type '{ name: string; }' is missing the following properties
//        from type '{ name: string; } & { age: number; } & { email: string; }':
//        age, email

コンパイル時間とメモリ

大規模なプロジェクトになると、こうした差はコンパイル時間にも響いてくる。extends で積み上げた interface は速い。交差型で組み立てた type は、やや重い処理になる。
// interface - 高速
interface ApiResponse<T> {
  data: T;
  status: number;
}

interface UserResponse extends ApiResponse<User> {
  user: User;
}

// type - やや重い処理
type ApiResponse<T> = {
  data: T;
  status: number;
};

type UserResponse = ApiResponse<User> & {
  user: User;
};
型の解決とキャッシュの仕組みも違う。同じ名前の interface は、一度解決されるとキャッシュされる。宣言マージも効率的に処理され、マージ後の型は一度だけ計算される。
interface Config {
  url: string;
}

// 同じ名前のinterfaceは一度解決されるとキャッシュされる
// 宣言マージも効率的に処理される
interface Config {
  timeout: number;
}

// 最終的にマージされた型が一度だけ計算される
対して type の交差型は、毎回計算が必要になる。ネストが深くなるほど、計算コストもじわじわ増していく。
type Config = {
  url: string;
};

type ExtendedConfig = Config & {
  timeout: number;
};

// 毎回交差型の計算が必要
// ネストが深くなると計算コストが増加
type DeeplyNestedConfig = ExtendedConfig & {
  retries: number;
} & {
  headers: Record<string, string>;
};
測定例としては、大量の型チェックが必要な場合で interface が約50ms、type が約65msといった差になる。3割ほど遅い計算だ。もっとも、これはあくまで目安で、実際の値はプロジェクトの規模による。
// 測定用のコード例
interface IUser {
  id: string;
  name: string;
  email: string;
}

type TUser = {
  id: string;
  name: string;
  email: string;
};

// 大量の型チェックが必要な場合
// interface: ~50ms
// type: ~65ms
// (実際の値はプロジェクトの規模による)
メモリ使用量も同様だ。interface はシンボルテーブルで効率的に管理される。一方の type は、型の展開のために追加のメモリを食う場合がある。 パフォーマンスの観点だけで見れば、大規模プロジェクトでは interface を基本にすることになる。継承の多い設計でも interface が有利だし、ライブラリのAPIも interface で設計するのがよい。複雑な型操作は、そもそもその機能が必要なのだから type で問題ない。 冒頭の「理由がなければ type」と食い違う、と思う人もいるかもしれない。だが、この差が効いてくるのは相当な規模になってからの話だ。15msの差に目くじらを立てるより、コードレビューのコメントを一つ減らす方がよほど速い。普通の業務アプリケーションなら気にするほどではない、というのが僕の見立てである。

tscの処理の流れ

もう少し踏み込んでみる。TypeScriptコンパイラ(tsc)は、interface と type をどういう段階を踏んで処理しているのか。

まずは字句解析である。次のようなソースコードがあったとする。

// ソースコード
interface User {
  name: string;
}

type Status = "active" | "inactive";
これがトークンの列に分解される。interface は INTERFACE_KEYWORD から、type は TYPE_KEYWORD から始まる。
// トークン化の結果
INTERFACE_KEYWORD, IDENTIFIER(User), LEFT_BRACE,
IDENTIFIER(name), COLON, STRING_KEYWORD, RIGHT_BRACE

TYPE_KEYWORD, IDENTIFIER(Status), EQUALS,
STRING_LITERAL("active"), PIPE, STRING_LITERAL("inactive")
次に構文解析で、トークン列からAST(抽象構文木)が作られる。ここで両者は別の種類のノードになる。interface は InterfaceDeclaration、type は TypeAliasDeclaration だ。
// interface の AST ノード
{
  kind: SyntaxKind.InterfaceDeclaration,
  name: { text: "User" },
  members: [
    {
      kind: SyntaxKind.PropertySignature,
      name: { text: "name" },
      type: { kind: SyntaxKind.StringKeyword }
    }
  ]
}

// type の AST ノード
{
  kind: SyntaxKind.TypeAliasDeclaration,
  name: { text: "Status" },
  type: {
    kind: SyntaxKind.UnionType,
    types: [
      { kind: SyntaxKind.LiteralType, literal: { text: "active" } },
      { kind: SyntaxKind.LiteralType, literal: { text: "inactive" } }
    ]
  }
}
続くバインディングでは、シンボルテーブルが構築される。interface のシンボルはメンバーを持つ。対して type のシンボルは、エイリアス先の型を指すだけである。
// interface のシンボル
Symbol {
  name: "User",
  flags: SymbolFlags.Interface,
  declarations: [InterfaceDeclaration],
  members: Map {
    "name" => Symbol { name: "name", type: StringType }
  }
}

// type のシンボル
Symbol {
  name: "Status",
  flags: SymbolFlags.TypeAlias,
  declarations: [TypeAliasDeclaration],
  aliasedSymbol: UnionType
}
そして型チェックだ。interface の場合、型チェッカーはまず User シンボルを解決し、オブジェクトリテラルの型を推論する。そのうえで構造的な互換性を確かめ、足りないプロパティを見つけ出す。
interface User {
  name: string;
  age: number;
}

const user: User = { name: "Alice" }; // エラー検出

// 型チェッカーの処理:
// 1. User シンボルを解決
// 2. オブジェクトリテラルの型を推論
// 3. 構造的互換性をチェック
// 4. 不足プロパティ(age)を検出
type の場合は、まずエイリアスを展開して実際の型構造を解決する。それからオブジェクトリテラルと比較する。名札で引くか、箱を開けて中身を見るか。平たく言えば、そういう違いなのだ。
type UserType = {
  name: string;
  age: number;
};

const user: UserType = { name: "Alice" }; // エラー検出

// 型チェッカーの処理:
// 1. UserType エイリアスを展開
// 2. 実際の型構造を解決
// 3. オブジェクトリテラルとの比較
// 4. 型の不一致を検出

最後がJavaScriptへの変換(Emit)だ。次のTypeScriptコードをコンパイルするとどうなるか。

// interface の定義
interface User {
  name: string;
  age: number;
  greet(): string;
}

// type の定義
type Config = {
  apiUrl: string;
  timeout: number;
};

// 実装
class UserImpl implements User {
  constructor(public name: string, public age: number) {}

  greet(): string {
    return `Hello, I'm ${this.name}`;
  }
}

const config: Config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
};

function processUser(user: User): void {
  console.log(user.greet());
}
結果は拍子抜けするほど単純だ。interface も type も跡形もなく消え、実装だけが残る。
// interface は完全に削除される
// interface User { ... } → コンパイル結果に含まれない

// type も完全に削除される
// type Config = { ... } → コンパイル結果に含まれない

// 実装のみが残る
class UserImpl {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  greet() {
    return `Hello, I'm ${this.name}`;
  }
}

const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
};

function processUser(user) {
  console.log(user.greet());
}

宣言マージと継承の内側

宣言マージがコンパイラの中でどう処理されるかも見ておく。たとえば、Window を二回に分けて拡張するコードがあるとする。
interface Window {
  customProperty: string;
}

interface Window {
  anotherProperty: number;
}

// 使用例
window.customProperty = "test";
window.anotherProperty = 42;
コンパイラは、最初の宣言で Window シンボルを作る。二つ目の宣言では既存のシンボルを見つけ、そこにメンバーを追加する。結果として、もともとの Window の全プロパティに、二つのプロパティが足された型ができあがる。
// 1. 最初の Interface Declaration を処理
// Symbol "Window" を作成、customProperty を追加

// 2. 二番目の Interface Declaration を処理
// 既存の "Window" Symbol を発見
// anotherProperty を既存のメンバーに追加

// 3. 最終的にマージされた型
interface Window {
  customProperty: string;
  anotherProperty: number;
  // + 既存のWindowの全プロパティ
}

コンパイル後に残るのは、やはり実装だけである。

// 型情報は削除され、実装のみ残る
window.customProperty = "test";
window.anotherProperty = 42;
継承も同じ要領で処理される。Animal を継承した Dog を、Labrador クラスが実装する例で考えよう。
interface Animal {
  name: string;
  makeSound(): string;
}

interface Dog extends Animal {
  breed: string;
  wagTail(): void;
}

class Labrador implements Dog {
  constructor(public name: string, public breed: string) {}

  makeSound(): string {
    return "Woof!";
  }

  wagTail(): void {
    console.log("Wagging tail happily");
  }
}
コンパイラは、まず Animal のシンボルを作る。Dog のシンボルには Animal を基底型として持たせ、継承したメンバーと固有のメンバーをまとめる。そのうえで、クラスがすべての必須メンバーを実装しているかを検証する。
// 1. Animal インターフェースのシンボル作成
AnimalSymbol {
  members: {
    name: StringType,
    makeSound: FunctionType
  }
}

// 2. Dog インターフェースの継承処理
DogSymbol {
  baseTypes: [AnimalSymbol],
  members: {
    // Animal から継承
    name: StringType,
    makeSound: FunctionType,
    // Dog 固有
    breed: StringType,
    wagTail: FunctionType
  }
}

// 3. クラス実装の検証
// すべての必須メンバーが実装されているかチェック

コンパイル後は、クラスの実装だけが残る。

class Labrador {
  constructor(name, breed) {
    this.name = name;
    this.breed = breed;
  }

  makeSound() {
    return "Woof!";
  }

  wagTail() {
    console.log("Wagging tail happily");
  }
}
type の交差型ならどうか。User と Timestamp を交差させた型を使う例を見てみる。
type Timestamp = {
  createdAt: Date;
  updatedAt: Date;
};

type User = {
  id: string;
  name: string;
};

type UserWithTimestamp = User & Timestamp;

const user: UserWithTimestamp = {
  id: "1",
  name: "Alice",
  createdAt: new Date(),
  updatedAt: new Date(),
};

コンパイラは、それぞれの型情報を保存しておく。交差型は、構成要素のリストとして持つだけだ。プロパティがひとつにまとめられるのは、実際に使われる時点である。

// 1. 各 type の型情報を保存
TimestampType = ObjectType {
  properties: {
    createdAt: DateType,
    updatedAt: DateType
  }
}

UserType = ObjectType {
  properties: {
    id: StringType,
    name: StringType
  }
}

// 2. 交差型の計算
UserWithTimestampType = IntersectionType {
  types: [UserType, TimestampType],
  // 実際の使用時に以下に展開される
  resolvedProperties: {
    id: StringType,
    name: StringType,
    createdAt: DateType,
    updatedAt: DateType
  }
}

そしてコンパイル後には、ただのオブジェクトリテラルが残るだけだ。

const user = {
  id: "1",
  name: "Alice",
  createdAt: new Date(),
  updatedAt: new Date(),
};

型消去

ここまで見てきたとおり、TypeScriptの最も重要な特徴のひとつは、コンパイル時にすべての型情報が消えることだ。消えるものの筆頭は型注釈である。

// コンパイル前
const name: string = "Alice";

// コンパイル後
const name = "Alice";
interface の定義も完全に消える。
// コンパイル前
interface User {
  name: string;
}

// コンパイル後
// 完全に削除される
type の定義も同じく消える。
// コンパイル前
type Status = "active" | "inactive";

// コンパイル後
// 完全に削除される

ジェネリクスも型パラメータごと消え去る。

// コンパイル前
function identity<T>(arg: T): T {
  return arg;
}

// コンパイル後
function identity(arg) {
  return arg;
}
逆に残るのは、実際の値と実装、ランタイムで必要になるクラス定義、そして値として使われる場合のenumだ。つまり、interface と type のどちらを選んでも、実行時の挙動にはなんの違いもない。名札も箱も、実行前にきれいに片づけられてしまう。違いが出るのは、あくまで書き味とコンパイラの都合なのである。

実践での組み合わせ

実際のコードでは、どちらか一方だけで済ませるより、両者を組み合わせることになる。たとえばAPIレスポンスの型定義なら、共通部分は interface の継承で組み立てる。成功と失敗のどちらかを表す部分だけを、type のユニオン型で束ねるのだ。
// ベースとなるレスポンス型
interface BaseApiResponse {
  status: number;
  message: string;
}

// 成功時のレスポンス
interface SuccessResponse<T> extends BaseApiResponse {
  data: T;
}

// エラー時のレスポンス
interface ErrorResponse extends BaseApiResponse {
  error: string;
}

// ユニオン型で組み合わせ
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse;
コンポーネントのPropsも同様だ。基本のPropsを interface で定義し、バリアントごとのPropsを交差型で作り、最後にユニオン型でまとめる。variant が "primary" なら color を渡せない、といった制約を型で表せるのは、なかなかにいい感じである。
// 基本的なprops
interface BaseButtonProps {
  children: React.ReactNode;
  onClick: () => void;
}

// バリアント別のprops
type PrimaryButtonProps = BaseButtonProps & {
  variant: "primary";
  color?: never;
};

type SecondaryButtonProps = BaseButtonProps & {
  variant: "secondary";
  color: "blue" | "red" | "green";
};

// ユニオン型で組み合わせ
type ButtonProps = PrimaryButtonProps | SecondaryButtonProps;

判断の順序

それでも迷ったら、次の順で決めればよい。まず、チームの規約、つまり既存のコードベースとの統一性を優先する。次に、Googleスタイルガイドに倣い、オブジェクト型は interface、それ以外は type にする。 それでも決まらなければ、宣言マージや継承が必要かという機能要件で判断する。最後の最後で迷ったら、type にしておく。制限が少なく、後から変えやすいからだ。 なお、ライブラリを開発する場合は、拡張性を重んじて interface を選ぶことが多い。利用者側が declare module で型を広げられるからである。
// ライブラリ側
export interface Config {
  apiUrl: string;
  timeout?: number;
}

// ユーザー側で拡張可能
declare module "your-library" {
  interface Config {
    customOption?: boolean;
  }
}

機能の対応表

最後に、ここまでの違いを表にしておく。

項目interfacetype
オブジェクト型定義✅ 推奨✅ 可能
ユニオン型/プリミティブ❌ 不可✅ 推奨
宣言マージ✅ 可能❌ 不可
マップ型❌ 不可✅ 可能
継承✅ extends✅ 交差型
タプル型❌ 不可✅ 可能
条件型❌ 不可✅ 可能
この表に沿って言えば、オブジェクトの型定義とライブラリの拡張可能な型には interface を使う。プリミティブ・ユニオン・タプルと複雑な型操作には type を使う。これが推奨されるアプローチになる。 以上、interface と type の違いを、書き方からコンパイラの内側まで一通り眺めてきた。名札で引くか、箱を開けて見るか。違いは確かにあるが、どちらも実行時にはきれいさっぱり消えてしまう。となれば、最後に効いてくるのは技術的な優劣ではない。チームの規約とプロジェクトの要件に沿って、一貫して使い続けること。それに尽きるのである。