これまでの回で、TypeScriptが提供する基本的な型(`string`, `number`, `boolean` など)や、配列、オブジェクトの型推論について学びました。しかし、実際のアプリケーション開発では、これらのプリミティブ型だけでは表現しきれない複雑なデータ構造を扱うことがほとんどです。
この第3回では、TypeScriptの強力な機能の一つである「カスタム型」の定義方法を学びます。具体的には、オブジェクトの形状を定義するための「インターフェース(Interface)」と、既存の型に新しい名前を与える「型エイリアス(Type Alias)」に焦点を当てます。これらを習得することで、より複雑なアプリケーションの型定義を柔軟かつ安全に行えるようになります。
デザイナーカスタム型って聞くと難しそうだけど、どんなことができるようになるの?
エンジニアはい、プリミティブ型だけでは表現しきれない複雑なデータを、より分かりやすく安全に扱えるようになりますよ。
インターフェース(Interface)
インターフェースは、オブジェクトの「形状」を定義するための強力なツールです。どのようなプロパティを持ち、それぞれのプロパティがどのような型であるべきかを明確に指定できます。これにより、オブジェクトが特定の構造を持っていることを保証し、開発中のエラーを早期に発見できます。
基本的なインターフェースの定義
`interface`キーワードを使って、オブジェクトのプロパティ名とその型を定義します。
interface User {
id: number;
name: string;
email: string;
}
const user1: User = {
id: 1,
name: "Alice",
email: "alice@example.com",
};
const user2: User = {
id: 2,
name: "Bob",
// emailプロパティがないためエラーになります
// Type '{ id: number; name: string; }' is not assignable to type 'User'.
// Property 'email' is missing in type '{ id: number; name: string; }' but required in type 'User'.
};
console.log(user1);
上記の例では、`User`インターフェースが`id`(`number`型)、`name`(`string`型)、`email`(`string`型)の3つのプロパティを持つオブジェクトの形状を定義しています。`user2`のように、インターフェースで定義されたプロパティが不足している場合、TypeScriptはエラーを報告します。
オプションプロパティ
オブジェクトのプロパティの中には、常に存在するとは限らないものもあります。そのような場合は、プロパティ名の後ろに`?`(疑問符)を付けることで、オプションプロパティとして定義できます。
interface User {
id: number;
name: string;
email?: string; // emailはオプション
}
const user3: User = {
id: 3,
name: "Charlie",
}; // emailがなくてもエラーにならない
const user4: User = {
id: 4,
name: "David",
email: "david@example.com",
}; // emailがあってもOK
console.log(user3);
console.log(user4);
読み取り専用プロパティ
オブジェクトの一部のプロパティが、一度初期化された後は変更されるべきではない場合、`readonly`キーワードを使用して読み取り専用プロパティとして定義できます。これは、特にIDのような不変であるべきデータに役立ちます。
interface Product {
readonly id: string; // idは読み取り専用
name: string;
price: number;
}
const product1: Product = {
id: "p001",
name: "Laptop",
price: 120000,
};
product1.name = "Gaming Laptop"; // nameは変更可能
// product1.id = "p002"; // エラー: Cannot assign to 'id' because it is a read-only property.
console.log(product1);
メソッドの定義
インターフェースは、プロパティだけでなく、オブジェクトが持つべきメソッドのシグネチャ(名前、引数、戻り値の型)も定義できます。
interface Greetable {
name: string;
greet(message: string): void; // greetメソッドのシグネチャを定義
}
const person: Greetable = {
name: "Eve",
greet(message: string) {
console.log(`${message}, my name is ${this.name}.`);
},
};
person.greet("Hello"); // 出力: Hello, my name is Eve.
インターフェースの拡張
`extends`キーワードを使うことで、既存のインターフェースの定義を再利用し、新しいプロパティを追加して拡張したインターフェースを作成できます。これにより、コードの重複を避け、保守性を高めることができます。
interface Person {
name: string;
age: number;
}
// Personインターフェースを拡張してEmployeeインターフェースを作成
interface Employee extends Person {
employeeId: string;
department: string;
}
const employee1: Employee = {
name: "Frank",
age: 30,
employeeId: "E001",
department: "Sales",
};
console.log(employee1);
デザイナーインターフェースはオブジェクトの設計図って感じかな。プロパティが足りないとエラーになるのがすごく便利ね!
エンジニアそうですね、オブジェクトの構造を明確にすることで、開発中のミスを防ぎ、保守性の高いコードを書く手助けになります。
型エイリアス(Type Alias)
型エイリアスは、`type`キーワードを使って既存の型に新しい名前を付ける機能です。インターフェースと同様にオブジェクトの形状を定義することもできますが、プリミティブ型、ユニオン型、タプル型、関数型など、あらゆる種類の型に名前を付けることができる点で、インターフェースよりも柔軟です。
基本的な型エイリアスの定義
オブジェクトの形状を型エイリアスで定義する例です。インターフェースと似ていますが、`type`キーワードを使用します。
type Point = {
x: number;
y: number;
};
const coordinate: Point = { x: 10, y: 20 };
console.log(coordinate);
プリミティブ型やリテラル型のエイリアス
プリミティブ型や特定のリテラル値にも名前を付けることができます。
type ID = string | number; // IDはstring型またはnumber型
type StatusCode = 200 | 400 | 500; // StatusCodeは特定のリテラル数値のみを許可
const userId: ID = "abc-123";
const productId: ID = 456;
const successStatus: StatusCode = 200;
// const invalidStatus: StatusCode = 404; // エラー: Type '404' is not assignable to type '200 | 400 | 500'.
console.log(userId, productId, successStatus);
ユニオン型と交差型
型エイリアスは、ユニオン型 (`|`) や交差型 (`&`) を定義する際に特に役立ちます。
- ユニオン型: 複数の型のいずれか一つであることを示します。
- 交差型: 複数の型を組み合わせて、それらすべてのプロパティを持つ新しい型を作成します。
type AdminUser = User & { role: "admin" }; // User型にroleプロパティを追加
type GuestUser = { readonly sessionId: string };
type AuthenticatedUser = AdminUser | GuestUser; // AdminUserかGuestUserのいずれか
const admin: AdminUser = {
id: 5,
name: "Grace",
email: "grace@example.com",
role: "admin",
};
const guest: GuestUser = {
sessionId: "s-789",
};
const currentUser: AuthenticatedUser = admin;
const anotherUser: AuthenticatedUser = guest;
console.log(currentUser);
console.log(anotherUser);
関数型の定義
型エイリアスを使って、関数のシグネチャを定義することもできます。これは、特定の型の関数を引数として受け取る場合などに便利です。
type GreeterFunction = (name: string) => string;
const sayHello: GreeterFunction = (name: string) => {
return `Hello, ${name}!`;
};
const sayGoodbye: GreeterFunction = (name: string) => {
return `Goodbye, ${name}.`;
};
console.log(sayHello("Hannah"));
console.log(sayGoodbye("Ivy"));
デザイナー型エイリアスはもっと自由度が高いのね!プリミティブ型にも名前を付けられるなんて知らなかったわ。
エンジニアはい、ユニオン型や関数型など、様々な型に新しい名前を与えることで、コードの意図をより明確に表現できます。
インターフェースと型エイリアスの使い分け
インターフェースと型エイリアスはどちらもオブジェクトの形状を定義できますが、いくつかの違いがあります。
- インターフェース:
- 主にオブジェクトの形状を定義するために使われます。
- `extends`キーワードで拡張できます。
- 宣言のマージ: 同じ名前のインターフェースを複数定義すると、それらのプロパティは自動的に結合(マージ)されます。これはライブラリが既存の型に新しいプロパティを追加する際などに利用されます。
- 型エイリアス:
- オブジェクトだけでなく、プリミティブ型、ユニオン型、タプル型、関数型など、あらゆる種類の型に名前を付けられます。
- `&`(交差型)を使って複数の型を組み合わせることができます。
- 宣言のマージはできません。同じ名前の型エイリアスを複数定義するとエラーになります。
一般的な推奨事項:
- オブジェクトの形状を定義し、拡張の可能性がある場合(特にライブラリ公開など): `interface` を使用するのが一般的です。宣言のマージ機能は特にライブラリ開発で強力です。
- オブジェクト以外の型(ユニオン型、タプル型、プリミティブ型など)に名前を付けたい場合: `type` を使用します。
- どちらでも良い場合: `interface` から始めるのが、拡張性や宣言のマージの恩恵を受けやすい点で、やや推奨される傾向にあります。ただし、最近では`type`の機能も非常に強力になり、どちらを使うかはプロジェクトの慣習や個人の好みに依るところも大きいです。
デザイナー結局、インターフェースと型エイリアス、どっちを使えばいいの?
エンジニアオブジェクトの形状を定義し、将来的な拡張を見込むならインターフェース、より柔軟に様々な型に名前を付けたいなら型エイリアス、という使い分けが一般的ですね。
まとめ
第3回では、TypeScriptで独自のカスタム型を定義するための2つの主要な方法、インターフェースと型エイリアスについて学びました。
- インターフェースは、オブジェクトの形状を定義し、オプションプロパティ、読み取り専用プロパティ、メソッド定義、そして`extends`による拡張をサポートします。
- 型エイリアスは、あらゆる型の定義に名前を付けられ、特にユニオン型や交差型、関数型の定義でその柔軟性を発揮します。
これらのカスタム型を使いこなすことで、複雑なデータ構造を持つアプリケーションにおいても、高い型安全性とコードの可読性、保守性を確保できるようになります。次回以降では、これらのカスタム型をさらに活用し、より実践的なTypeScriptの書き方を学んでいきましょう。