オブジェクト

【TypeScriptで学ぶ】オブジェクト入門

unit-code@wp-admin

こんにちは、めんだこです!

今回は、JavaScriptでの中心的概念であるオブジェクトについて復習しながら、TypeScriptでの基本的な取り扱い方について、再確認できればと思います!

この記事では、まず一章でJavaScriptでのオブジェクトの基本を復習し、優れた点・欠点を再確認します。
その後の章では、TypeScriptの導入によって、どのように欠点を克服していくかを見ていく、といった構成になっています。

JavaScriptについて、ある程度習熟されている方は、1章は飛ばして読んでいただいて構いません。

少し長い記事になってしまいましたが、これからTypeScriptを学ぶ方はもちろん、ReactやVue.jsなどでTypeScriptを使っている方にとっても役に立つ内容となったと思います。

では、よろしくお願いします!

1

JavaScriptにおけるオブジェクト

まずは、JavaScriptにおけるオブジェクトについて、見てみましょう。

オブジェクトは、簡単に言えば変数や関数を管理する入れ物です。

さらに、それらを組み合わせることでプログラムの複雑な構造を表現することができ、JavaSctriptにおける中心的な概念となっています。

「オブジェクトを制するものは、JavaScriptを制する」と言っても過言ではなさそうです。

ここでは、オブジェクトの初期化(宣言)方法から、基本的な操作方法を復習し、JavaScriptにおけるオブジェクトの問題点を探っていこうと思います。

1.1 オブジェクトの初期化

早速、簡単な例を見てもらいたいので、オブジェクトを用いて軽く自己紹介をしてみます。

let mendako = {
  name: "mendako",
  legs: 8,
  isOctopus: true,
  address: {
    ocean: "Pacific",
    trench: "Mariana",
  },
  greet: function() {
    console.log(`Hello, I'm ${this.name}.\nI live in the ${this.address.ocean} Ocean.`)
  },
};

mendako.greet();

実行結果をnodeで確認してみましょう。


node script.js
Hello, I'm mendako.
I live in the Pacific Ocean.

このように、オブジェクトは変数や関数を

プロパティ(キー): 値

の基本構造を {} で囲うことで、格納・管理することができます。

なお、{} の部分を「オブジェクトリテラル」といいます。
ざっくりと、「オブジェクトを表現するための書き方の作法」と読み替えても構いません。

先ほどのオブジェクトは、このような構造を持っていました。

それぞれの構造を 「,(カンマ)」 で区切ることで、複数のプロパティと値の組を格納し、オブジェクトを初期化していることがわかります。

1.2 オブジェクトの基本的な操作

では、JavaScriptにおける基本的なオブジェクトの操作方法を見てみましょう。

いくつか種類がありますので、順番に説明していきます。

1. ドット記法

オブジェクトとプロパティを「.(ドット)」で繋ぐことで、値を取得・更新、追加することができます。

Q
記法の詳細

取得と更新

console.log(mendako.legs); // output: 8

// 足の本数を、8 -> 10 に更新
mendako.legs = 10;

// isOctopusを、足の本数で判定する関数に更新
mendako.isOctopus = function() {
    return this.legs == 8;
};

console.log(mendako.isOctopus()); // output: false

追加

mendako.isCute = true;

console.log(mendako.isCute); // output: true

注意点

必ず、プロパティ名を直接指定する必要があります。

const prop = "name";

console.log(mendako.prop) // output: undefined

この場合、mendakoのpropというプロパティにアクセスしに行っている為、出力で「mendako」は得られず、定義されていないことを示す「undefined」が表示されます。

2. ブラケット記法

オブジェクトの後ろに[ ]を続けて、その中に文字列でプロパティ名を指定することで、値を取得・更新、追加することができます。

Q
記法の詳細

取得と更新

console.log(mendako["legs"]); // output: 8

// 足の本数を、8 -> 10 に更新
mendako["legs"] = 10;

// isOctopusを、足の本数で判定する関数に更新
mendako["isOctopus"] = function() {
    return this.legs == 8;
};

console.log(mendako["isOctopus"]()); // output: false

追加

mendako["isCute"] = true;

console.log(mendako["isCute"]); // output: true

注意点

プロパティ名に変数や定数を指定できます。

const prop = "name";

console.log(mendako[prop]) // output: mendako

この場合、[]内のpropがまず評価され、mendako[“name”]として、呼び出されます。

よって、出力として「mendako」が得られるのです。

3. プロパティの削除

オブジェクトのプロパティを削除する際は、delete演算子を使います。

使い方は簡単で、ドット記法、ブラケット記法のどちらでも使えます。

delete mendako.legs

console.log(mendako.legs) // output: undefined
delete mendako["legs"]

console.log(mendako["legs"]) // output: undefined

1.3 JavaScriptにおけるオブジェクトの問題点

ここまで、JavaScriptにおけるオブジェクトを見てきましたが、実際に開発する上では、次の問題点があります。

オブジェクトの構造が動的に変化する

プロパティを開発者が自由に追加、削除できてしまうため、定義していないプロパティにアクセスしてしまい、エラーが実行時まで分からない、といったことが発生する確率が上がります。

また、あると思っていたプロパティが削除されている、というようなケースも考えられます。

柔軟に構造を変更できることは、動的型付け言語の良さでもありますが、大規模な開発などでは、開発者同士で自由に改変できてしまうオブジェクトが厄介な相手になることがあります。

(オブジェクトに限らずですが)JavaScriptは良くも悪くも柔軟であるため、開発の初期段階ではスピード感を持って開発できる反面

  • 実行までエラーを見つけにくい
  • ソースコード上のどこかで改変されている可能性を考慮し続けるので、開発者の脳のリソースが喰われる

といった欠点を抱えています。

この問題を解決するために開発されたのがTypeScriptです。
次からは、TypeScriptでのオブジェクトの扱い方について見ていきましょう!

コラム オブジェクトのコピー

JavaScriptを使ったことのある方は、すでにご存知かも知れませんが、オブジェクトをコピーする際には注意が必要です。

なお、この項目はJavaScriptに限らず、TypeScriptでも同様です。

Q
詳細(スキップ可能)

まずは、こちらの例をみて、最後の行の出力がどうなるか考えてみましょう。

const user = {
  age: 30,
};

let another = user;

another.age = 28;

console.log(user.age); // output: ?

実際に実行します。


node script.js
28

この挙動は少し不自然に思えますが、5行目でuserオブジェクトをコピーしたとき、実は同じオブジェクトを参照しています。
よって、片方に変更が加わると、もう一方の変数(なんと定数ですら!)変更されてしまうのです。

オブジェクトと変数・定数の参照イメージ
オブジェクトと変数・定数の参照イメージ

この注意が必要な挙動に対して、これまではスプレッド演算子を用いて別々のオブジェクトを作成する方法などが使われてきましたが、ネストしたオブジェクトをコピーするのが依然として面倒でした。

しかし、現在ではオブジェクトの構造をまるっと全てコピーする便利なAPIが用意されました。
それが、structuredClone()です。

const user = {
  age: 30,
  name: {
    first: "foo",
    last: "bar",
  }
};

let another = structuredClone(user);

console.log("user:   ", user);
console.log("another:", another);
console.log(user === another);

another.name.last = "hoge";
console.log("user:   ", user);
console.log("another:", another);

実行してみると、それぞれがちゃんと別々のオブジェクトを参照していることが分かります。


node script.js
user:    { age: 30, name: { first: 'foo', last: 'bar' } }
another: { age: 30, name: { first: 'foo', last: 'bar' } }
false
user:    { age: 30, name: { first: 'foo', last: 'bar' } }
another: { age: 30, name: { first: 'foo', last: 'hoge' } }

今後、オブジェクトのコピーを作成する場合は、積極的にstructuredClone()を使っていくのが望ましいと思います。

2

TypeScriptにおけるオブジェクト

ここからは、TypeScriptにおけるオブジェクトについて掘り下げていきたいと思います。

まずは、通常のオブジェクトリテラルによる型推論と、型注釈による型の指定方法から見ていきます。
こちら(特に型注釈)は、そこまで使われる機会は多くはないですが、最も基本的なオブジェクトの宣言方法になりますので、紹介はさせていただきます。

その後、実際の現場でよく使われる型エイリアス(type)や、インターフェイス(interface)による構造の定義方法や、両者の違いについても解説します。

注意点
ここからはTypeScriptの解説に入りますので、ファイルの拡張子は「.ts」となっています。
また、node上でTypeScriptを実行するにあたって、ts-nodeを使用しています。
この関係で、tsファイルの出力コマンドも「npx ts-node (file-name)」となっています。

2.1 オブジェクトリテラルによる型推論

TypeScriptは静的型付け言語ですが、すべての型を宣言する必要はなく、JavaScriptと同様にオブジェクトを宣言しても自動的に型を推論してくれます。

const user = {
  name: "charlie",
  age: 32,
};

// 型推論
// {
//   name: string;
//   age: number;
// }

この例では、定数userのオブジェクトを宣言していますが、型を明示的に指定していません。
しかし、TypeScriptは与えられた値から、自動的に型を推論しています。

JavaScriptとは異なる点は、主に2つ挙げられます。

1. 存在しないプロパティへのアクセス

JavaScriptでは、存在しないプロパティへアクセスしようとすると、「undefined」が返されますが、TypeScriptでは、次のようにコンパイルエラーが発生します。

console.log(user.address);

npx ts-node script.ts
error TS2339: Property 'address' does not exist on type '{ name: string; legs: number; isOctopus: boolean; }'.

意訳)addressなんてプロパティは存在しないよ。

2. 異なる型の代入

javaScriptでは、プロパティに固有の型を持っていなかったため、別の型へ変更することができていましたが、TypeScriptでそれは許されていません。

user.age = "年齢"

npx ts-node script.ts
error TS2322: Type 'string' is not assignable to type 'number'.

意訳)number型のところに、string型は代入できないよ。

以上が、TypeScriptにおける最も簡単なオブジェクトの宣言方法です。

JavaScriptで宣言するよりも、型付けによって安全性が担保されていることがわかると思います。

2.2 型注釈による型の指定

TypeScriptでは、オブジェクトのプロパティを明示的に宣言する方法も用意されています。

先ほどのオブジェクトを「型注釈」をつけて記述すると、次のようになります。

const user: {
  name: string;
  age: number;
} = {
  name: "charlie",
  age: 32,
};

前半部分の{ … }内でプロパティとその型を指定し、後半部分の{ … }で値を設定しています。

しかし、先ほど少し言及したように、この書き方は冗長になりがちで、再利用性に欠け、意図も理解しづらいため、あまりお勧めはしません。

まずは、後述する「型エイリアス(type)」か「インターフェイス(interface)」による記述法を検討みるのが良いでしょう。

文法自体は、TypeScriptの基本的な文法

変数: 型 = 値

を使っており、これは、次の型エイリアスの時も同様の構造を持っています。

2.3 型エイリアス(type)を用いた構造定義

TypeScriptの型エイリアス(type)を用いたオブジェクトの構造の定義方法について、みてみます。

型エイリアスは、TypeScriptの主要な機能の一つで、その名の通り、型に別名をつける機能です。

簡単な例を見てみましょう。先ほどのuserが持っていた型を型エイリアスで記述します。

// User型を定義
type User = {
  name: string;
  age: number;
};

// User型を使って、定数(変数)の型を指定
const charlie: User = {
  name: "charlie",
  age: 32,
};

const matt: User = {
  name: "matt",
  age: 61,
};

// 出力
console.log(charlie, matt);

はじめにUser型を定義し、それを2つの定数に充てています。

型注釈の際は、それぞれの変数に型を記述する必要がありましたが、今回はUser型を一度宣言することで、複数の変数に同じ型を適用できるようになりました。

なお、型エイリアスはコンパイル時に評価されるため、型宣言が後に来ても問題なく動作します。

// User型を使って、定数(変数)の型を指定
const charlie: User = {
  name: "charlie",
  age: 32,
};

const matt: User = {
  name: "matt",
  age: 61,
};

// User型を定義
type User = {
  name: string;
  age: number;
};

// 出力
console.log(charlie, matt); // 問題なく出力される

2.4 インターフェイス(interface)を用いた構造定義

インターフェイスは、オブジェクトの構造を定義するのに特化した機能です。

基本的な宣言方法は、型エイリアスと同様ですが、インターフェイスだけが持つ機能を持っていて、より汎用的に使える機能となっています。

では、インターフェイスの基本的な使い方を順番に見てみます。

1. 宣言方法

インターフェイスの宣言方法は、型エイリアスの場合と似ています。

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

// Userインターフェイスを使って、定数(変数)の型を指定
const charlie: User = {
  name: "charlie",
  age: 32,
};

const matt: User = {
  name: "matt",
  age: 61,
};

type時の宣言と異なるのは、「=」を使わずに宣言することです。
その他の構文は変わりません。

2. インデックスシグニチャ

「TypeScriptでも、JavaScriptのように柔軟にプロパティを追加したい。。
だけど、型による制限も残したい。。」

そういった要求に応えてくれるのがインデックスシグニチャです。

基本的な構文は次のようになります。

[キーの名前(任意): キーの型]: 値の型

例を挙げてみましょう。

interface FruitBasket {
  [i: string]: number;
}

const fruit: FruitBasket = {};

fruit.apple = 2;
fruit.banana = 4;

console.log(fruit); // output: { apple: 2, banana: 4 }

このように、プロパティと値の型を指定することで、あらかじめ宣言していなかったプロパティを使用することができます。

3. インターフェイスの拡張

インターフェイスの拡張機能では、既存のインターフェイスを基に、新たなインターフェイスをを派生させることができます。

定義する際には、extendsキーワードを使用します。
具体的に見てみましょう。

// 拡張の元
interface Drink {
  volume: number;
}

// Tea は Drink を拡張したインターフェイス
interface Tea extends Drink {
  isHot: boolean;
  color: string;
}

const greenTea: Tea = {
  volume: 500,
  isHot: true,
  color: "green",
};

この例では、飲み物のインターフェイスを拡張して、緑茶のインターフェイスを作成しています。

4. インターフェイスのマージ

あまり使われる機会はありませんが、インターフェイスは、マージという機能を持っており、同一のインターフェイスを2箇所で宣言することで発生します。

interface Tea {
  volume: number;
}

// 同じインターフェイスを再度宣言
interface Tea {
  isHot: boolean;
  color: string;
}

const oolongTea = {
  volume: 500,
  isHot: false,
  color: "brown",
};

インターフェイスのマージが必要になる機会は限られています。

無闇に使うと、インターフェイスの定義がソースコードに散らばってしまう危険もあるため、使用する際は注意が必要です。

このように、インターフェイスには、型エイリアスには無い汎用的な機能が存在します。

また、インターフェイスは、クラスの振る舞いを制限する目的で使用されることもありますが、今回は紹介は省かせていただきます。

3

参考文献

こちらの記事の執筆にあたり、以下の書籍を参考にさせていただきました。

ありがとうございました!

【書籍】

今回は、ご覧いただきありがとうございました。

また、次の記事でお会いしましょう!では!

より良い記事を提供するため、この記事に対するご指摘等ございましたら随時コメントを募集しています。

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


ABOUT ME
めんだこ
めんだこ
webエンジニア

バックエンドの技術を中心に、フロントエンドやインフラ周りの知識も記事にしています!
・プログラミングの学習法や知識
・エンジニアとして考えていること など
を共有して、より良いエンジニアライフを送るための情報を発信します!


新卒未経験でwebエンジニアとなり、翌年からフリーランスとして活動している、現在25歳。
未だにプログラミングが楽しくて仕方ないらしい。
記事URLをコピーしました