TL;DR
このページでは、Next.js と TypeScript の中で使われる "type" と "interface" の特性と使用方法について深く掘り下げます。
| 開発環境 | バージョン |
|---|---|
| Next.js | 13.4.3 |
| TypeScript | 5.0.4 |
| Emotion | 11.11.0 |
| React | 18.2.0 |
typeとinterface は、一言でいうと「型をまとめて定義して、再利用可能にする機能」です。これらは、共通の型定義を一箇所にまとめ、それを複数の場所で再利用することができます。これにより、コードの重複を避け、コードの一貫性を保つことができます。それでは、違いは何でしょうか?
Next.js の開発では、どちらを使うべきか?
Next.js の開発では interface を中心に使用し、必要に応じて type を使い分けることが多いです。インターフェースは、公開する API や、複数のクラスが実装すべき契約を定義するのに適しています。このため、Props の型を定義する際にはインターフェースを使用します。
一方、型エイリアス(type)は、複雑な型操作やユニオン型(和型)、タプル型、その他の高度な型を定義する場合に優れています。これは React のコンポーネントで頻繁には使用されないかもしれませんが、高度な型操作が必要な場合には有用です。
type と interface の違い
type と interface は、TypeScript で型情報を定義するための 2 つの主要な方法です。それぞれの使い方と特性を見ていきましょう。
type は、TypeScript の基本的な型定義機能です。type は文字通り任意の型を定義することができます。
type User = {
id: number
name: string
}
一方で、interface は、より具体的なオブジェクトの型定義に特化しています。interface を用いると、クラスが特定の構造を持つことを保証する契約を作ることができます。
interface User {
id: number
name: string
}
type |
interface |
|
|---|---|---|
| 基本的な用途 | 任意の型を定義する | オブジェクトの型を定義する |
| 合成 | & 演算子を使う |
extends キーワードを使う |
| 再定義 | 不可 | 可(同名の interface を続けて定義すると、自動的にマージされる) |
| Utility Types | 適用可能(Partial, Required, etc) |
適用不可 |
| その他 | 型のエイリアス(alias)としても使用可 | クラスやオブジェクトリテラルと結合して使用可能 |
各行について少し詳しく説明すると、以下のようになります。
基本的な用途
type は任意の型を定義するのに使えますが、interface はオブジェクトの型を定義するのに特化しています。
合成
type では & 演算子を使って型を合成しますが、interface では extends キーワードを使って既存の interface を拡張します。
再定義
type は一度定義すると再定義できません。一方で interface は再定義可能で、同名の interface を複数回定義するとそれらが自動的にマージされます。
Utility Types
type は TypeScript の Utility Types(例:Partial, Required 等)と組み合わせて使うことができますが、interface ではそれができません。
その他
type は型エイリアスとして使うことができますが、interface はクラスやオブジェクトリテラルと結合して使うことができます。
以上の情報を元に、あなたのプロジェクトでどちらを使うべきか、より明確な決定ができるはずですよね。
Next.js で type と interface を使う
Next.js プロジェクトでは、TypeScript の力を借りてコードの品質を保つことができます。ここでは type と interface を用いた Next.js のコンポーネントの例を見ていきましょう。
ファイル名:/components/UserCard.tsx
import React from 'react'
type UserProps = {
id: number
name: string
}
const UserCard = ({ id, name }: UserProps) => (
<div>
<p>{id}</p>
<p>{name}</p>
</div>
)
export default UserCard
この例では UserCard コンポーネントが UserProps という型を持つプロパティを受け取ることを宣言しています。ここで type を使っています。
interface を使った例
一方、interface を使うと、より複雑な型の管理が可能になります。クラスとの相互作用を考える際には、特に interface の利点が生きてきます。
interface IUser {
id: number
name: string
logIn(): void
}
class User implements IUser {
id: number
name: string
constructor(id: number, name: string) {
this.id = id
this.name = name
}
logIn() {
console.log(`${this.name} is logged in.`)
}
}
上記のコードは、IUser interface を実装する User クラスを定義しています。このように interface を使うと、特定のクラスが必要なプロパティとメソッドを持つことを保証できます。
オーバーライド
プロパティのオーバーライドについて説明しましょう。この概念は特にinterfaceの文脈で重要となります。
interface でのプロパティのオーバーライド
interfaceでは、既存の interface を拡張して新しい interface を作ることができます。この時、同名のプロパティを持つ場合、そのプロパティの型は新しい interface で指定した型にオーバーライド(上書き)されます。この特性は、既存の型定義を柔軟にカスタマイズするのに役立ちます。
次の例を見てみましょう。
interface User {
id: number
name: string
}
interface Admin extends User {
name: string[] // 'name' property is overridden
}
const admin: Admin = {
id: 1,
name: ['John Doe', 'Admin'], // 'name' is now an array of strings
}
上記のコードでは、User interface を継承した Admin interface が定義されています。しかし、Admin interface では name プロパティの型が string から string[] に変更されています。このように interface を使用すると、既存の型定義を新しい要件に合わせてオーバーライドすることが可能になります。
この性質は非常に便利な反面、型の互換性を維持するためには注意が必要です。互換性が保たれないオーバーライドは、予期せぬエラーを引き起こす可能性があります。
type の場合、同様のオーバーライドは直接的にはサポートされていませんが、Utility Types を利用することで似たような挙動を模倣することができます。
このように、TypeScript の type と interface はそれぞれ異なる特性を持つため、状況に応じて適切な方を選択することが大切です。この辺りが JavaScript の世界で成長するためのコツ、と言えるでしょうね。
Emotion での実装
Emotion は CSS-in-JS ライブラリの一つで、React コンポーネントにスタイルを適用する際に、type や interface が役立つ場面が多くあります。以下にその一例を示します。
import { css } from '@emotion/react'
interface ButtonProps {
primary?: boolean
}
const Button = ({ primary = false }: ButtonProps) => {
const buttonStyles = css`
background: ${primary ? 'blue' : 'white'};
color: ${primary ? 'white' : 'black'};
`
return <button css={buttonStyles}>Click me</button>
}
この Button コンポーネントは、primary プロパティを受け取り、それに基づいてボタンのスタイルを動的に決定します。Emotion の css 関数はテンプレートリテラルとして使われ、スタイル定義を行います。
Mapped Types
Mapped Types は TypeScript の高度な機能で、新しい型を生成するために使用されます。これは type を使用した機能で、interface では直接サポートされていません。
Mapped Types は、他の型から新しい型を"マッピング"することができます。以下にその例を示します。
type User = {
id: number
name: string
}
type PartialUser = {
[P in keyof User]?: User[P]
}
// PartialUser is now equivalent to:
// type PartialUser = {
// id?: number;
// name?: string;
// }
上記の例では、User 型から PartialUser 型を生成しています。この PartialUser 型は、User 型のすべてのプロパティをオプショナル(つまり、存在しなくても良い)にします。これを達成するために、keyof キーワードと in 演算子を使用しています。
また、TypeScript は一部の一般的な Mapped Types をビルトインとして提供しています。例えば上記の Partial<T> のように、型 T のすべてのプロパティをオプショナルにすることができます。
type User = {
id: number
name: string
}
type PartialUser = Partial<User>
// PartialUser is now equivalent to:
// type PartialUser = {
// id?: number;
// name?: string;
// }
非常に直訳的な表現ですが、Mapped Types はある意味での型のためのmapメソッドと言えます。ただし、JavaScript のArray.prototype.mapメソッドが配列の各要素を操作するのに対して、Mapped Types は型の各プロパティを操作します。
具体的には、Mapped Types は元の型の各プロパティに何らかの操作を適用し、新しい型を生成します。その操作は型のレベルで行われ、元の型のプロパティがどのように変換されるかを制御します。
例えば、以下の Mapped Types は元の型の全てのプロパティをオプショナルにします。
type Partial<T> = {
[P in keyof T]?: T[P]
}
ここで[P in keyof T]?: T[P];という部分が Mapped Types のコアです。この部分では、型Tの各プロパティPに対してT[P]型のオプショナルなプロパティを生成します。結果として、新しいPartial<T>型は元の型Tと同じプロパティを持ちますが、すべてのプロパティがオプショナルになります。
Mapped Types を使うことで、元の型のプロパティを任意の方法で変換した新しい型を容易に作ることができます。これにより、コードの再利用性と可読性が向上します。
これらのビルトインの Mapped Types は非常に便利で、より複雑な型を簡単に作成するのに役立ちます。そのため、TypeScript を深く理解していくにつれて、これらの高度な機能をより効果的に使用できるようになるでしょう。
以上のように、Next.js と TypeScript を使った開発において、type と interface の違いを理解し、それぞれが最適な場面で使えるようになることが、より堅牢で読みやすいコードを書くための一歩になるでしょう。