TypeScriptにおける型の定義
TypeScriptで型を定義するとき、typeとinterfaceどちらを使えばいいんだ?と思うことはありませんか?
両方とも型を定義する方法ですが、用途の違いがあるのでこの記事ではそれぞれの特徴と使い所を解説します。
typeとは
type文は型に名前をつける文です。
type 型名 = 型;のように使用します。
例えば、下記のように宣言することで、FooBarObj型を定義することができます。
type FooBarObj = {
foo: number;
bar: string;
};
// objの型としてFooBarObj型を指定
const obj: FooBarObj = {
foo: 123,
bar: "Hello, world!",
};
type文はすでにある型に別名をつけるということができ、型エイリアスと呼ばれます。
例えば、type文はオブジェクト型ではなくプリミティブ型に対しても利用でき、
このことからも、「型に別名をつける」という動きが理解しやすいかと思います。
type UserId = string;
// id: stringと同義
const id: UserId = "123"
interfaceとは
interface宣言は型名を新規作成する方法です。
interface宣言ではtype文とは違い、オブジェクト型のみを扱います。
先ほどtype文で書いたFooBarObj型をinterfaceで宣言すると、下記のように書くことができます。
interface FooBarObj {
foo: number;
bar: string;
}
// objの型としてFooBarObj型を指定
const obj: FooBarObj = {
foo: 123,
bar: "Hello, world!",
};
typeとinterfaceの違い
オブジェクト型を宣言する場合の方法としては大きな違いはないように見えますが、
両者にはいくつかの違いが存在します。
その1つが継承です。
interfaceは継承が可能ですが、type文は継承ができません。
一方で、type文でも交差型(&)を使用することで継承と似たことを実現することはできます。
interface Animal {
name: string;
}
type Creature = {
dna: string;
};
// Dogはname, dna, dogTypeのプロパティを持つ
interface Dog extends Animal, Creature {
dogType: string;
}
また、type文では同名のものを複数定義できませんが、interfaceは同名のinterfaceを定義でき、
同名の定義をすべて合成したinterfaceになります。
ただし、同名のプロパティの型定義が異なる場合はコンパイルエラーになります。
// 名称が重複しているためコンパイルエラーが発生
type SameNameTypeWillError = {
message: string;
};
type SameNameTypeWillError = {
detail: string;
};
interface SameNameInterfaceIsAllowed {
myField: string;
sameNameSameTypeIsAllowed: number;
sameNameDifferentTypeIsNotAllowed: string;
}
interface SameNameInterfaceIsAllowed {
newField: string;
sameNameSameTypeIsAllowed: number;
}
interface SameNameInterfaceIsAllowed {
// すでに定義済みのプロパティと同名で型定義が異なるためコンパイルエラーが発生
sameNameDifferentTypeIsNotAllowed: number;
}
他にもMapped Typesと呼ばれる型の再利用方法はtype文のみでしか使用できないという違いがあります。
Mapped Typesは下記のように、主にユニオン型と組み合わせて使用することができます。
type SystemSupportLanguage = "en" | "fr" | "it" | "es";
type Butterfly = {
[key in SystemSupportLanguage]: string;
};
上記では、Butterflyの型は下記と同義になります。
type Butterfly = {
en: string;
fr: string;
it: string;
es: string;
};
そのため、下記のようにSystemSupportLanguageで定義されているリテラル型以外のプロパティを追加するとコンパイルエラーが発生します。
const butterflies: Butterfly = {
en: "Butterfly",
fr: "Papillon",
it: "Farfalla",
es: "Mariposa",
// deはButterflyに存在しないためコンパイルエラーが発生
de: "Schmetterling",
};
Mapped Typesを使用することでキーの制約を与えることができますが、
これはinterfaceでは使用できず、type文のみで使用できます。
結局どちらを使うべきなのか
どちらを使うべきなのかという点に関しては明確な正解はないようです。
Googleが公開しているTypeScriptのスタイルガイドでは、
プリミティブな値やユニオン型やタプルの型定義をする場合は型エイリアスを利用し、
オブジェクトの型を定義する場合はインターフェースを使うことを推奨しています。
また、下記のようなそれぞれの主張も存在します。
type派
- interfaceでできることは大抵typeでもできる
- 知らないうちに拡張されたくないから最初からtypeでよい
interface派
- オブジェクトの形状を表現するのに適している
- 拡張が可能
私が読んだプロを目指す人のためのTypeScript入門では「特定の場合を除いてtype文のみを使っておけば困ることはない」だろうと書かれていましたが、
結局はチームやプロジェクト内でルールを決めてそれを遵守するのが良さそうです。