はじめに
TypeScriptのオブジェクト型は、オブジェクトの構造を記述するための型であり、オブジェクトがどのようなプロパティを持ち、それぞれのプロパティがどのような型を持つかを定義します。
この備忘録では、オブジェクト型についてまとめます。
基本的なオブジェクト型
オブジェクトの構造を定義するには、型エイリアス(type alias)またはインターフェース(interface)を使用します。
両者は似ていますが、一般的にオブジェクトの型を記述する場合はインターフェースの使用が推奨されているらしい。
型エイリアス
type User = {
name: string;
age: number;
};
const user: User = {
name: "Alice",
age: 30,
};
インターフェース
interface Product {
id: number;
name: string;
price: number;
};
const product: Product = {
id: 1,
name: "Laptop",
price: 1200,
};
なんでインターフェースが推奨されるのか?
インターフェースはオブジェクト指向プログラミングの概念により近く、拡張性や柔軟性が高いため、オブジェクトの型を定義する際に推奨されています。
逆に、型エイリアスは、インターフェースでは表現できないプリミティブ型やユニオン型、タプル型、交差型などの複雑な型に名前をつけたい場合に推奨されます。
インターフェースはオブジェクトの構造を定義するのに特化しているため、これらを扱うことができません。
インターフェースはextends
を使って他のインターフェースを継承できるため、既存の型を再利用しながら新しい型を定義できます。
また、同じ名前のインターフェースを複数宣言すると自動的に統合(マージ)されるDeclaration Mergingという機能があり、ライブラリの型定義を拡張する際などに非常に便利です。
一方、型エイリアスはこのような拡張やマージができません。
そのため、オブジェクトの型を定義する際には、将来的な拡張やライブラリとの連携を考慮してインターフェースが推奨されるみたいです。
インターフェースの拡張(extends
)
新しいインターフェースが既存のインターフェースのプロパティをすべて継承し、独自のプロパティを追加できます。
interface Animal {
name: string;
}
interface Dog extends Animal { // Animalを継承
breed: string;
}
const myDog: Dog = {
name: "ポチ",
breed: "柴犬",
};
インターフェースのマージ(Declaration Merging)
同じ名前のインターフェースを複数定義すると、TypeScriptはそれらを自動的に1つにまとめます。
interface User {
name: string;
}
interface User { // 同じ名前のインターフェースを再度宣言
age: number;
}
// 2つの宣言がマージされ、User型はnameとageの両方を持つ
const user: User = {
name: "Alice",
age: 30,
};
これは、外部ライブラリの既存の型に新しいプロパティを追加したい場合に特に役立ちます。
オプショナルプロパティと読み取り専用プロパティ
オブジェクトのプロパティを、オプショナル(任意)や読み取り専用(readonly)に設定できます。
-
オプショナルプロパティ: プロパティ名の後ろに
?
を付けることで、そのプロパティがオブジェクトに存在しなくてもエラーにならないようにする -
読み取り専用プロパティ:
readonly
キーワードを付けることで、そのプロパティがオブジェクトの初期化後に変更できないようにする
interface Car {
readonly brand: string;
model: string;
year?: number; // オプショナルなプロパティ
};
const myCar: Car = {
brand: "トヨタ",
model: "カムリ",
};
// myCar.brand = "ホンダ"; // エラー: 'brand' は読み取り専用プロパティのため、値を代入できません。
インデックスシグネチャ
インデックスシグネチャを使用すると、プロパティ名が動的に決まるオブジェクトの型を定義できます。
interface Dictionary {
[key: string]: string; // すべての文字列キーが、文字列の値を持つことを定義
};
const myDictionary: Dictionary = {
"apple": "りんご",
"book": "本",
};
console.log(myDictionary["apple"]); // 出力: "りんご"
これは、プロパティ名が事前にわからないオブジェクトの形を定義するために使われます。
言い換えると、「キーが特定の型で、値も特定の型である」というルールをオブジェクト全体に設定するものです。
たとえば、次のようなユーザーIDとユーザー名を紐づけるオブジェクトがあるとします。
const users = {
"user_101": "Alice",
"user_102": "Bob",
"user_103": "Charlie"
};
この場合、users
オブジェクトのキー("user_101"
, "user_102"
, "user_103"
)は文字列型で、それぞれのキーに対応する値("Alice"
, "Bob"
, "Charlie"
)も文字列型です。
このオブジェクトの型を定義する場合、インデックスシグネチャを使うと、個々のプロパティをすべて記述する必要がなくなります。
interface UserNames {
[key: string]: string;
}
const userNames: UserNames = {
"user_101": "Alice",
"user_102": "Bob",
};
このコードでは、[key: string]: string
の部分がインデックスシグネチャです。
-
key: string
:キーが文字列型であることを示す -
string
:そのキーに対応する値が文字列型であることを示す
この定義により、userNames
オブジェクトは、どんな文字列をキーとして使っても、その値は必ず文字列でなければならない、というルールが適用されます。
ただし、インデックスシグネチャにはいくつかの制限があります。
- キーの型:キーには
string
、number
、またはsymbol
のいずれかしか使えない - プロパティの整合性:インデックスシグネチャが定義されているオブジェクトに、シグネチャと異なる型のプロパティを直接追加することはできない
交差型(Intersection Types)
複数のオブジェクト型を交差型(&
)を使って結合し、新しい型を作成できます。
これにより、結合されたすべての型のプロパティを新しい型が持つようになります。
type Person = {
name: string;
age: number;
};
type Employee = {
employeeId: number;
department: string;
};
type Manager = Person & Employee; // 両方の型のプロパティを結合
const manager: Manager = {
name: "Bob",
age: 45,
employeeId: 101,
department: "営業",
};