TypeScript: Type alias vs Interface

今日はInterfaceについて。Type aliasと似ているらしいので比較しながら調べていく。

準備

TypeScriptのコードはそのままブラウザのConsoleには書けない。
https://www.typescriptlang.org/playで試しながら見ていこう。

Interfaceとは

そもそもインターフェースとは、「接点」などの意味を持つ言葉である。インターフェースはTypeScriptだけのものではなく普通の言葉なので、USBとかBluetoothもPCと周辺機器を繋いでいるインターフェースだし、電気のコンセントやAPI、あるいはUX/UIの”UI”もユーザーインターフェースの略だったりと、インターフェースという言葉の守備範囲は結構広い。TypeScriptにおけるInterfaceもコード同士を指定した形で繋げたり関連づけたりするための「接点」としての役割を担っている。

TypeとInterfaceの違い

Type aliasとInterfaceは置き換え可能なことが多いらしい。どんな感じか試すため、まずは先日のブログ記事で型(Type)について取り上げた時のtype定義のコードサンプルをInterfaceに置き換えてみる。

TypeScript
type Person = {
  name: string;
  gender: "male" | "female";
  country: Country;
  birthday: Date;
  hasPet: boolean;
  petDetail?: Pet[];
}

type Pet = {
  name: string;
  id?: string;
  birthday?: Date;
}

type Country = "USA" | "Japan" | "Other";

TypeScript
interface Person {
    name: string;
    gender: "male" | "female";
    country: Country;
    birthday: Date;
    hasPet: boolean;
    petDetail?: Pet[];
}

interface Pet {
    name: string;
    id?: string;
    birthday?: Date;
}

type Country = "USA" | "Japan" | "Other";

type Person = {(略)} のようにイコールの右辺がオブジェクトの場合、ほぼそのままinterface Person {(略)} に置き換えられる。イコールがいらない
ただ、type Country = “USA” | “Japan” | “Other” のように右側がオブジェクトではない時はinterfaceが使えないのでtypeを使う必要がある。
Interfaceには制約があると言われると、Typeの方が使い道が多くて便利なのでは?と思ってしまう。Interfaceを使った方がいい場合というのはどういう場合なのだろう。

Interfaceの特徴とリスク

Type aliasとInterfaceは置き換え可能といっても、同じ用途のものをいくつも用意したわけではなく、その目的が異なる。データの形状を指定して、Type Safeなコードにすることを目的にしているTypeに対し、Interfaceはコード同士を結び付けやすくすることがその機能の目的である。Interfaceはコード同士を結びつけるために、拡張のしやすさという特徴を持っている。しかしこの特徴を知らずに使ってしまうと意図しない結果を招くことがあるようだ。

extendと&について

Interfaceの拡張のしやすさという特徴を理解するために、Type aliasとInterfaceそれぞれの拡張の方法をまずは見ていく。TypeScriptでは既存のインターフェースや型を元に、新しいインターフェースや型を作ることができる。Type aliasでは&を使い、Interfaceではextendsを使うと誤解していたのだが、Type aliasでは&を使い、Interfaceではextendsと&どちらを使ってもいいらしい。それぞれの例を以下に示す。

TypeScript
// Type Alias with "&"

type Pet = {
  name: string;
  id?: string;
  birthday?: Date;
}

//Platypusという型はPetのtypeにisSecretAgentを加えたもの
type Platypus = Pet & {
  isSecretAgent: boolean;
}

//Petのtypeではname以外は必須ではないのでこれでOK
const Perry: Platypus = {
  name: "Perry",
  isSecretAgent: true
}
TypeScript
// Interface with "extends"

interface Pet {
  name: string;
  id?: string;
  birthday?: Date;
}

// PlatypusはPetのinterfaceを基本としつつ、Platypus特有のプロパティisSecretAgentを加える
interface Platypus extends Pet {
  isSecretAgent: boolean;
}

const Perry: Platypus = {
  name: "Perry",
  isSecretAgent: true
}
TypeScript
// Interface with "&"

interface Pet {
  name: string;
  id?: string;
  birthday?: Date;
}

//Platypus特有のプロパティisSecretAgentを作成
interface Platypus {
  isSecretAgent: boolean;
}

//PetとPlatypusのinterfaceを合体
const Perry: Pet & Platypus = {
  name: "Perry",
  isSecretAgent: true
}

結局Type alias、Interface、いずれの書き方でも拡張できているように見えるがInterfaceの拡張のしやすさは、どういうことか。実は上の例はPetを元にして、新たにPlatypusを定義しているので、厳密には拡張ではなくInheritance(継承)と呼ぶらしい。ここでいう拡張とは一度定義した後に、後からプロパティを追加することを指す。

拡張できなくても継承できるなら充分では?という素人(わたし)の感想は置いておいて、Type aliasでは拡張ができない(= 同じ名前で中身を変えたり付け足したりして再定義することはできない)。
一方で、Interfaceは定義の拡張ができてしまうので拡張がしやすいとうことか。イメージ的にはType aliasはJavaScriptの”const(定数)”、Interfaceは”let(変数)”に似ている感じ。しかも、&とか、extendsとかのキーワードも無しで、そのまま同じ名前で書いただけで拡張できてしまうというお手軽さに危険が潜む。

TypeScript
// Type Alias

type Pet = {
  name: string;
  id?: string;
  birthday?: Date;
}

//同じ名前で中身を変えたり付け足したりして
//再定義することはできない
type Pet = {
  isSecretAgent: boolean;
}

//ERROR : Duplicate identifier 'Pet' となってしまう

TypeScript
// Interface

interface Pet {
  name: string;
  id?: string;
  birthday?: Date;
}

//同じ名前でプロパティisSecretAgentを加えることができてしまう
interface Pet {
  isSecretAgent: boolean;
}

const Perry: Pet = {
  name: "Perry",
  isSecretAgent: true
}

複数人で作業しているときや、ファイルがたくさんある場合などは、うっかり拡張コードを書いてしまうなど注意が必要というのは納得。ネーミングとかも似たような感じのものがたくさんできてしまうようなことも多いし、新しいもの作ったつもりが実は同じ名前のが他にもあるのにエラーにならないのは確かに怖い。というわけでライブラリなど不特定多数のユーザーがコードを参照してユーザー側のコードに適合させていくような場合でない限り、Type aliasを使った方がType safeなコードを書けそうというのがここまでの感想。

never型

Type aliasにも気をつけなければならない点があったので付け加えておく。
例えばPet にもPlatypusにもidというプロパティを持たせ、Petの方は string、Platypusの方はnumberと定義した。型が一致しませんよ、みたいなエラーは(出てきてほしいのに)ここでは出てこなかったのだが、13行目でnumberの型でidを渡したらエラーが出た。Type ‘number’ is not assignable to type ‘never’. 調べたところ”&”を使うとき、矛盾があってもコンパイルエラーにはならないが、never型になってしまうらしい。

TypeScript
type Pet = {
  name: string;
  id?: string;
  birthday?: Date;
}

type Platypus = Pet & {
  id: number;
  isSecretAgent: boolean;
}

const Perry: Platypus = {
  id: 111, // ここにエラーが出る:Type 'number' is not assignable to type 'never'.
  name: "Perry",
  isSecretAgent: true
}

そんな時は以下のように書き直すと、Petはstring | number いずれかの idを持つかもしれないが、Platypusの時はnumber型のidが必ず必要というようにPetの種類によって、条件を狭めることができる。

TypeScript
type Pet = {
  name: string;
  id?: string | number;// 変更箇所
  birthday?: Date;
}

type Platypus = Pet & {
  id: number;
  isSecretAgent: boolean;
}

const Perry: Platypus = {
  id: 111, // idは必須且つnumber型しか受け取れないという条件に合致するのでOK
  name: "Perry",
  isSecretAgent: true
}

Webアプリのフロントエンド開発においては、とりあえずType aliasで書いていけば良さそう。interfaceはAPIとかサーバー側の方でたまに見かける気がするけど、今度もう少し観察してみる。


最近北野エースで見つけたフムス(ハムス)が美味しかった!
こんな風に売ってるフムスは大して美味しくないものが多いが、これは意外とちゃんとしてる。

hummus

類似投稿