1
1

【TypeScript】'interface'と'type'の使い分けをする前に特徴と注意点

Posted at

はじめに

ともに型定義に使われますが、特徴があるので使う際はぜひ見ていってください:wink:

// インターフェイスの宣言
interface Person {
    name: string;
    age: number;
}

//型エイリアスによるオブジェクト型の宣言
type Person = {
    name: string;
    age: number;
};  //セミコロンあり、省略可

特徴

interfaceの特徴
:ballot_box_with_check: 拡張したい時は、extends を使う。
:warning:extendsで拡張した型は全て含める必要がある。

:ballot_box_with_check: 同じ名前で複数定義でき、拡張しやすい
:warning:自動的にマージされるため、同じ名前のinterfaceでプログラム全体に散らばると個々の定義がどのように結合されているのか分かりにくくなるので注意。

:ballot_box_with_check: 実装の詳細を含めることはできない。
:warning:実装するには クラスでimplements を用いて実装する。

型エイリアスtypeの特徴
:ballot_box_with_check: 拡張したい時は、&(交差型) を使う。(※extendsは使えない)
:ballot_box_with_check: 同じ名前で複数定義できない。
:ballot_box_with_check: 様々な型に名前をつける事ができる。
:ballot_box_with_check: 再利用性が高く、複雑な型構造に対応しやすい。

interfaceとtypeの拡張の仕方の違い

interfaceの場合:extendsによる拡張
interface A {
  x: number;
}

// extendsを用いて継承 
interface B extends A {
  y: string;
}

const example: B = {
  x: 10,
  y: "hello",
};
typeの場合:交差型(&)による拡張
type A = {
  x: number;
};

type B = {
  y: string;
};

// ① 型エイリアス B に A を拡張したい場合
type B = A & {
    y: string;
};
const example: B = {
    x: 10,
    y: "hello",
};

// ②別の型エリアス C に拡張したい場合
type C = A & B;
const example: C = {
    x: 10,
    y: "hello",
};

interfaceは同じ名前で複数定義できる

同じ名前のinterfaceによるマージ(合成)
// 同じ名前のinterfaceを複数回定義すると、自動的にマージされる。
interface A {
  x: number;
}

interface A {
  y: string;
}

const example: A = {
  x: 10,
  y: "hello",
};

同じ名前のinterfaceを再定義できるからこそ、重複するプロパティを持つ場合には注意。

同じ名前のinterfaceによるマージ(合成):重複するプロパティを持つ場合
interface Person {
  name: string;
  age: number; // 1回目の定義での age
}

interface Person {
  age: string;  // 2回目の定義での age  (1回目と型が違う)
  address: string;
}

const person: Person = {
  name: "John",
  age: "30",  // 整合性を保つため、エラーが発生
  address: "123 Main St"
};

一方、typeを同じ名前で複数定義しようとすると、

同じ名前で複数のtypeは定義できない
type A = {
  x: number;
}

type A = {
  y: string;
}
// Duplicate identifier 'A'. エラーが出る。

typeでは作成後、同じ名前で追加できないので注意

同名のinterfaceが散らばると結合が分かりにくくなる?

interface Foods {
  name: string;
  price: number; 
}

////長い処理////

//////////////

const food: Foods = {
  name: "Apple",
  price: 100,
};

"長い処理"の中にinterfaceの再定義が紛れてしまうと気付きづらいので注意。

interface Foods {
  name: string;
  price: number; 
}

////長い処理////  ↓長い処理の中に再定義されたinterfaceがあるような場合
interface Foods{
  calories: number;
}
//////////////

// この時点で Foods は次のように結合されている:
// interface Foods {
//   name: string;
//   price: number;
//   calories: number;
// }

// もし、長い処理中にcaloriesプロパティがある事に気づかず含めていないとエラーになる
const food: Foods = {
  name: "Apple",
  price: 100,
//  calories: 158
};

定義されたインターフェース全てのプロパティを含める必要があるので注意

ところで、オプショナルプロパティ「?」(プロパティが任意で省略可能)を含んでいたらどうなのか?
////長い処理////
interface Foods{
  calories?: number;// オプショナルな場合
}
//////////////
interface Foods {
  name: string;
  price: number; 
}

// 長い処理の中で再定義されたinterfaceがある
interface Foods{
  calories?: number;// オプショナルな場合
}

//エラーは出ない
const food: Foods = {
  name: "Apple",
  price: 100,
};

オプショナルな場合は含めなくてもよい。

interfaceでは実装できない

interfaceでは実装できない
// interfaceでは実装できないため、クラスでimplementsを用いて実装する。

interface Animal {
    name: string;
    speak(): void; // interface内では実装できない
}

// DogクラスがAnimalインターフェースを実装
class Dog implements Animal {
    constructor(public name: string) {}
    // 実装
    speak(): void {
        console.log(`${this.name} says Woof! Woof!`); 
    }
}

const myDog = new Dog("Buddy");
myDog.speak(); // "Buddy says Woof! Woof!"
interfaceでは実装できない(複数指定したい場合)
interface Animal {
    name: string;
    speak(): void; // interface内では実装できない
}

interface Flyable {
    fly(): void; // interface内では実装できない
}

// 複数指定したい時は 「,」 でインターフェースを区切り列挙する
class Bird implements Animal, Flyable  {
    constructor(public name: string) {}
    // 実装
    speak(): void {
        console.log(`${this.name} chirps`);
    }
    // 実装
    fly(): void {
        console.log(`${this.name} is flying`);
    }
}

const myBird = new Bird("Swallow");
myBird.speak(); // "Swallow chirps"
myBird.fly();   // "Swallow is flying"

両方のインターフェースで定義された全てのプロパティとメソッドを実装する必要があるので注意

typeはさまざまな"型"に名前をつける事ができる

// プリミティブ型
type Text = string;

// リテラル型
type ErrorCode = 400;

// 配列型
type NumberList = number[];

// オブジェクト型
type UserProfile = { 
    id: number; 
    name: string;
};

// ユニオン型
type NumberOrNull = number | null;

// 交差型(インタセクション型)
type TeamMember = User & {
    team: string;
};

// 関数型
type StringToBoolCallback = (value: string) => boolean;

// タプル型
type RGBColor = [number, number, number];

型エイリアスは同じ型を再利用したいときに使うと便利です。型の定義が一箇所になるため、保守性が向上します。また、型に名前を与えることで可読性が上がる場合があります。型に名前があると、その型が何を意味しているのかがコードの読み手に伝わりやすくなります。

型エイリアス (type alias) | TypeScript入門『サバイバルTypeScript』より

typeは再利用性が高く、複雑な型構造に対応しやすい

複雑な型構造
interface Animal {
  name: string; // 名前
}

interface Dog extends Animal {
  breed: string; // 品種
}

interface Bird extends Animal {
  canFly: boolean; //飛ぶか飛ばないか
}

type DogOrBirdWithAge = (Dog | Bird) & { age: number };
// Dog または Bird のどちらかの型に age プロパティを追加したもの
型の再利用性
//複数の型を組み合わせ共通のプロパティで定義したDogOrBirdWithAge型を再利用すると、
const myDog: DogOrBirdWithAge = {
  name: "Snoopy",
  breed: "Beagle",
  age: 5
};

const myBird: DogOrBirdWithAge = {
  name: "Tweety",
  canFly: true,
  age: 2
};

おまけ

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

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

interfaceとtypeの違い | TypeScript入門『サバイバルTypeScript』より

参考

詳しく違いを記事にされているものもあるので参考にしてみてください:relaxed:

型エイリアス (type alias) | TypeScript入門『サバイバルTypeScript』

interfaceとtypeの違い | TypeScript入門『サバイバルTypeScript』

interfaceとtypeの違い、そして何を使うべきかについて

インターフェースを実装する | TypeScript入門『サバイバルTypeScript』

TypeScriptのインターフェースを解説 - Recursion

TypeScriptのInterfaceとTypeの比較 #TypeScript - Qiita

[TypeScript] interface vs type(とそれぞれの違いについて)

1
1
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1