オブジェクト型はTypeScriptの基本的な概念であり、開発者が構造化された方法でデータを扱うことを可能にします。
TypeScriptにおけるプリミティブ型とは、単一の値を表すデータ型のことでした。プリミティブ型の例としては、数値、文字列、ブーリアン、そしてNULLやundefinedなどがあります。これらの型は言語に組み込まれており、追加のプロパティやメソッドを持っていません。
一方、オブジェクト型は、複数のプロパティとメソッドを持つことができるデータ型です。オブジェクト型はコンストラクタ関数を用いて作成することができ、開発者はオブジェクトのプロパティとメソッドを定義することができます。
TypeScriptでは、オブジェクト型はclassキーワードで定義することができます。
クラスは、オブジェクトの設計図を定義するために使用されます。この設計図は、異なるプロパティとメソッドを持つ同じオブジェクトタイプの複数のインスタンスを作成するために使用することができます。
クラスはコンストラクタ関数を提供し、これはオブジェクトのプロパティとメソッドを初期化するために使用されます。また、クラスは、OOPの基本概念である継承、ポリモーフィズム、カプセル化を利用することができます。
例えば、Dogというオブジェクト型の基本的な例を考えてみましょう。
class Dog {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
bark() {
console.log("Woof! Woof!");
}
}
この例では、2つのプロパティ(nameとage)と1つのメソッド(bark())を持つDogクラスを定義しています。このクラスを使って、nameとageのプロパティに異なる値を設定したDogオブジェクトのインスタンスを複数作成することができます。
let myDog = new Dog("Fido", 3);
let yourDog = new Dog("John", 4);
console.log(myDog.name); // "Fido"
console.log(yourDog.name); // 4
myDog.name = "Buddy";
console.log(myDog.name); // "Buddy"
オブジェクト型とプリミティブ型のもうひとつの大きな違いは、オブジェクト型がメソッドを持つことができる点です。メソッドはオブジェクト型に関連付けられた関数で、そのオブジェクト型の任意のインスタンスに対して呼び出すことができます。
上の例では、Dogクラスのインスタンスに対して呼び出すことができるbarkメソッドを定義しています。
let myDog = new Dog("Fido", 3);
myDog.bark(); // "Woof! Woof!"
オブジェクトリテラル{}は、JavaScriptやTypeScriptでオブジェクトを作成する方法の一つで、プロパティと値のセットを持つオブジェクトを素早く作成したい場合に使われます。
例えば、以下のようにオブジェクトリテラルを使ってDogオブジェクトを作成することができます。オブジェクトのプロパティの値には、プリミティブ型の値以外にも上記のように関数やオブジェクトも含めることができます。
let myDog = {
name: "Fido",
age: 3,
birthday: new Date("1990-01-01"),
bark: () => console.log("Woof! Woof!")
};
console.log(myDog.name); // "Fido"
console.log(myDog.age); // 3
console.log(myDog.birthday); // Date: "1990-01-01T00:00:00.000Z"
myDog.bark(); // "Woof! Woof!"
これによって、前の例で定義したDogクラスと同じプロパティとメソッドを持つオブジェクトが作成されます。しかし、クラスを使って作成したオブジェクト型とは異なり、オブジェクトリテラルにはコンストラクタ機能がないため、異なるプロパティを持つ同じオブジェクトの複数のインスタンスを作成するために使用することはできません。
オブジェクトにおける型注釈を行う方法についても見ておきましょう。上記で紹介したオブジェクトリテラルのような記法で型注釈を宣言できます。
let myDog: {
name: string,
age: number,
birthday: Date,
bark: () => void
} = {
name: "Fido",
age: 3,
birthday: new Date("1990-01-01"),
bark: () => console.log("Woof! Woof!")
};
他の例も見てみましょう。
let car: {
make: string,
model: string,
year: number,
getFullName: () => string
} = {
make: "Toyota",
model: "Camry",
year: 2020,
getFullName: () => {
return `${car.year} ${car.make} ${car.model}`;
}
};
console.log(car.make); // "Toyota"
console.log(car.model); // "Camry"
console.log(car.year); // 2020
console.log(car.getFullName()); // "2020 Toyota Camry"
typeキーワードは、新しい型の別名を作成するために使用します。既存の型に新しい名前を付けて、コード内でその型を参照しやすくします。
例えば、文字列型と数値型の和であるStringOrNumberという新しい型を作成してみましょう。
type StringOrNumber = string | number;
StringOrNumberとして宣言された変数や関数の引数はすべて、文字列か数値にすることができます。
let myVariable: StringOrNumber = "Hello";
myVariable = 42;
TypeScriptではオブジェクトのリテラルにも型を割り当てることができます。これは、オブジェクトの構造を記述する新しい型を作成する簡単な方法です。
例えば、Dogという型を作成して、オブジェクトリテラルの構造を記述することができます。
type Dog = {
name: string;
age: number;
bark: () => void;
}
これは、name, age, barkなどのプロパティやメソッドを持つオブジェクトの構造を記述するDogという新しい型を作成します。typeは、割り当てられた変数が正しいプロパティとメソッドを持つことを確認するために使用することができます。
let myDog: Dog = {
name: "Fido",
age: 3,
bark: function() {
console.log("Woof! Woof!");
}
}
TypeScriptでは、型を定義する際に、プロパティやパラメータの名前の後にクエスチョンマーク(?)をつけることで、オプションであることを指定することができます。これは、その型のオブジェクトを作成するときや、そのパラメータを持つ関数を呼び出すときに、そのプロパティやパラメータが存在しなくてもよいということを意味します。
例えば、名前、年齢、そしてオプションで電子メールアドレスを持つ人物オブジェクトの型を定義する場合を考えてみましょう。この場合、typeキーワードを使用し、emailプロパティに?を追加します。
type Person = {
name: string;
age: number;
email?: string;
}
let person1: Person = { name: 'John', age: 30 }
let person2: Person = { name: 'Jane', age: 25, email: 'jane@example.com' }
この型は、割り当てられた変数が正しいプロパティ(名前、年齢)を持ち、変数がEメールを持つ場合は文字列であることを確認するために使用することができます。
interfaceは、オブジェクトの形状を記述するもので、オブジェクトが持つべきプロパティやメソッドの種類を指定するために利用されます。
interface Dog {
name: string;
age: number;
bark(): void;
}
let myDog: Dog = {
name: "Fido",
age: 3,
bark: () => console.log("Woof!")
};
console.log(myDog.name);
console.log(myDog.age);
myDog.bark();
一般的には、オブジェクトの形状を記述するためにinterfaceが使われることが多く、プリミティブ型のエイリアスやタプル、カスタム型などオブジェクトの形状を記述しない型を作成するためにtypeが使われることが多くなっています。
TypeScriptでは、readonlyキーワードは、オブジェクトのプロパティが読み取り専用であること、つまりアクセスのみが可能で、変更はできないことを示すために使用されます。一度readonlyとマークされたプロパティは、最初に作成されたときにのみ値を割り当てることができ、後で再割り当てすることはできません。これは状態が不変なオブジェクトを作成する際に有用です。
interface Point {
readonly x: number;
readonly y: number;
}
let point: Point = { x: 0, y: 0 };
// xがreadonlyであるため、エラーとなります
point.x = 5;
console.log(point.x); // 0
console.log(point.y); // 0
オブジェクトをconstで宣言された変数に格納することで、オブジェクトへの参照が他のオブジェクトに再割り当てされないようにすることができますが、そのオブジェクトのプロパティはまだ変更できる点に注意が必要です。
const point = { x: 0, y: 0 };
point.x = 5; // 可能
console.log(point.x); // 5
インデックスシグネチャとは、特定の型を持つオブジェクトの構造を記述する方法です。これによって、オブジェクトが持つことのできるプロパティの型を指定することができます。
[index: type]: valueType;
indexはプレースホルダー名、typeはindexの型、valueTypeはプロパティがマップする値の型です。
それでは例を見てみましょう。
interface Person {
name: string;
age: number;
[index: string]: any;
}
let person: Person = {
name: "John Smith",
age: 35,
occupation: "Developer",
hobbies: ["programming", "reading"],
address: {
street: "123 Main St",
city: "Anytown",
state: "Anystate",
zip: 12345
}
};
インデックスシグネチャを使用すると、Personインターフェースに必要なプロパティを特別に定義することなく、追加で追加することができます。この例では、インデックスシグネチャを使って Personインターフェースにoccupation, hobbies, addressを追加しています。
JSON APIなどの外部ソースから取得したデータを扱う際に、すべてのプロパティの名前を事前に知っているわけではないが、値がどのような型であるべきかは知っている場合に有効なことがあります。
interface ApiResponse {
data: any;
[index: string]: any;
}
const apiResponse: ApiResponse = {
data: {
id: 1,
name: "John Smith"
},
status: "success",
message: "The data was retrieved successfully."
};
この例では、ApiResponseインターフェースは、2つのプロパティを持ちます。dataとインデックスシグネチャです。インデックスシグネチャによって、ApiResponseオブジェクトが、data以外のプロパティ、例えばstatusやmessageを持つことが可能になります。インデックスシグネチャはanyに設定されているので、これらの追加プロパティは任意の値を持つことができます。
Note: 高階関数については、CS基礎/中級/オブジェクトで詳しく学習できます。