Next.js と TypeScript で、オブジェクトを理解する

TwitterFacebookHatena

TL;DR

このページでは、オブジェクトの実装方法について解説しますね。オブジェクトとは、複数のデータを一つにまとめたものです。

開発環境 バージョン
Next.js 13.4.4
TypeScript 5.0.4
Emotion 11.11.0
React 18.2.0

オブジェクトとは?

オブジェクトはプログラミングの世界で頻繁に出てくる考え方で、データとそのデータに関連する機能をひとまとめにしたものを指します。JavaScript や TypeScript において、オブジェクトはキーと値のペアをまとめたもので、多くの場合、値はデータや関数を含むことができます。これにより、関連するデータと機能をグループ化し、コードの再利用性と可読性を高めることができます。

例えば、人物についてのデータをオブジェクトで表現すると以下のようになります。この user オブジェクトは name、age、greet の 3 つのプロパティを持ちます。

const user = {
  name: 'Taro',
  age: 25,
  greet: () => `Hello, I'm Taro.`,
}

例えば、人物についてのデータを TypeScript のオブジェクトで表現すると以下のようになります。

type User = {
  name: string
  age: number
  greet: () => void
}

const user: User = {
  name: 'Taro',
  age: 25,
  greet() {
    console.log(`Hello, I'm ${this.name}.`)
  },
}

この例では、User という型のオブジェクト user を定義しています。この user オブジェクトは name、age、greet という 3 つのプロパティを持ち、それぞれがユーザーの名前、年齢、挨拶の機能を表現しています。こういった形でオブジェクトを用いると、関連する情報や機能を一箇所にまとめることができ、コードの管理がしやすくなります。

プロパティアクセス

JavaScript と TypeScript では、オブジェクトのプロパティにアクセスするための 2 つの主な方法があります。それらは、ドット記法とブラケット記法です。

ドット記法

ドット記法は、プロパティ名が有効な識別子である場合に使用できます。例えば、次のように使用します。

let obj = { name: 'John', age: 30 }
console.log(obj.name) // "John"

この方法は、プロパティ名が事前にわかっている場合や、プロパティ名が有効な識別子である場合に便利です。

ブラケット記法

ブラケット記法は、プロパティ名が動的である場合や、有効な識別子でない場合(例えば、空白やハイフンを含む文字列)に使用できます。例えば、次のように使用します。

let obj = { name: 'John', age: 30, 'favorite color': 'blue' }
let propName = 'age'
console.log(obj[propName]) // 30
console.log(obj['favorite color']) // "blue"

この方法は、プロパティ名が変数に格納されている場合や、有効な識別子として表現できない文字列をプロパティ名として使用する場合に便利です。

TypeScript では、これらのアクセス方法を適切に使用することで、オブジェクトのプロパティに安全にアクセスすることができます。また、TypeScript の型検査機能により、存在しないプロパティにアクセスしようとするとコンパイル時にエラーを通知してくれます。

object 型と、{}型

TypeScript には、オブジェクトを表現するためのいくつかの方法があります。その中でも特によく使われるのが object 型と {} 型です。それぞれの特性と使い方について詳しく見ていきましょう。

object 型

object 型は、TypeScript で任意のオブジェクトを表すための型です。object 型を使用すると、任意のオブジェクトを受け入れることができます。

let obj: object
obj = { name: 'Taro' } // OK
obj = [1, 2, 3] // OK
obj = function () {} // OK

しかし、object 型はあまりにも広範で、それが持つプロパティに対するアクセスや、それが持つメソッドの呼び出しを安全に行うことができません。

let obj: object
obj = { name: 'Taro' }

console.log(obj.name) // Error: Property 'name' does not exist on type 'object'.

{}型

一方、{} 型もまたオブジェクトを表すための型ですが、これは「形状が全く定義されていないオブジェクト」を意味します。

let obj: {} // `obj` is an object with no known properties
obj = {} // OK
obj = { name: 'Taro' } // OK

ただし、{} 型のオブジェクトでは、それが持つプロパティに対するアクセスや、それが持つメソッドの呼び出しを行うことはできません。

let obj: {}
obj = { name: 'Taro' }

console.log(obj.name) // Error: Property 'name' does not exist on type '{}'.

オブジェクトのプロパティを型として定義する

オブジェクトのプロパティに対するアクセスを型安全に行うためには、そのオブジェクトが持つプロパティを型として定義する必要があります。このために TypeScript では、オブジェクトの形状を定義するためのinterfacetypeが提供されています。

interface Person {
  name: string
}

let obj: Person
obj = { name: 'Taro' } // OK

console.log(obj.name) // OK

ここでは、Person インターフェースを定義し、それを型として使用しています。これにより、name プロパティへのアクセスが型安全に行えます。

TypeScript のオブジェクトは、連想配列ではない

TypeScript のオブジェクトは、一部の特性を連想配列と共有していますが、それらは厳密に同じものではありません。

オブジェクトと連想配列

オブジェクトとは、キー(プロパティ名)とそれに関連づけられた値を持つデータ構造を指します。JavaScript や TypeScript でのオブジェクトは、この定義に従います。つまり、キーと値のペアを持つデータ構造です。

一方、連想配列(またはマップ、ハッシュマップ、辞書とも呼ばれます)もまた、キーと値のペアを保持するデータ構造です。しかし、連想配列は一般的には、キーとして任意のデータ型を許容し、キーの比較にはキーの内部構造を用います。

これは JavaScript のオブジェクトとは異なります。JavaScript のオブジェクトは基本的には文字列のキーのみを許容し(ES2015 からは Symbol も許容)、キーの比較には文字列としての等価性が用いられます。

TypeScript のオブジェクトと連想配列

TypeScript は JavaScript のスーパーセットであるため、JavaScript のオブジェクトの性質はそのまま TypeScript にも引き継がれています。ただし、TypeScript では更に厳密な型検査が可能であり、特定のキーと値の型を持つオブジェクトを定義することが可能です。

さらに TypeScript ではMapという組み込みオブジェクトも提供されており、これは真の意味での連想配列(ハッシュマップ)として機能します。Mapオブジェクトは、任意の値をキーとして許容し、キーと値のペアの挿入と検索を高速に行うことができます。

結論として、TypeScript のオブジェクトは、一部の特性を連想配列と共有していますが、それらは全く同じものではありません。しかし、Mapオブジェクトを使用することで、TypeScript でも真の連想配列の機能を利用することができます。

三項演算子

オブジェクトリテラルの中でも三項演算子を使用することが可能です。オブジェクトのプロパティの値を動的に設定するためによく使われます。

以下に一例を示します。

let isVIP = true

let user = {
  name: 'John',
  age: 30,
  accessLevel: isVIP ? 'VIP' : 'Regular',
}

console.log(user.accessLevel) // "VIP"

この例では、isVIPという真偽値に基づいてaccessLevelプロパティの値を動的に設定しています。isVIPtrueの場合、accessLevelの値は"VIP"になります。それ以外の場合、"Regular"になります。

このような動的なプロパティ設定は、オブジェクトの作成時に状態や条件によってそのプロパティ値を変えたい場合に特に便利です。また、JavaScript や TypeScript では三項演算子を用いることで簡潔にこのような条件分岐を表現することができます。

スプレッド構文

スプレッド構文を使うと、オブジェクトの作成時にプロパティを別のオブジェクトからコピーすることができます。

スプレッド構文を使用したオブジェクトリテラルの例をご紹介いたします。スプレッド構文は、オブジェクトのプロパティや配列の要素を展開する際によく用いられます。

const baseConfig = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
}

const devConfig = {
  ...baseConfig,
  debugMode: true,
}

console.log(devConfig)
// Output: { apiUrl: 'https://api.example.com', timeout: 5000, debugMode: true }

上記の例では、baseConfigというオブジェクトを持っており、開発環境の設定であるdevConfigに対して、baseConfigのすべてのプロパティをコピーしています。その上で、開発環境特有の設定debugModeを追加しています。

これは、複数の環境(開発環境、テスト環境、本番環境など)で共通する設定とそれぞれの環境固有の設定を管理する際に便利なパターンです。スプレッド構文を使用することで、既存のオブジェクトを簡単にコピーし、新たなプロパティを追加したり既存のプロパティを上書きしたりすることができます。

アロー関数

アロー関数を使用したオブジェクトリテラルの例を紹介いたします。オブジェクトリテラルのプロパティには、値だけでなく、関数を割り当てることも可能です。アロー関数を用いると、シンタックスが簡潔になり、thisのスコープについても直感的に扱うことができます。

以下に、オブジェクトリテラル内でアロー関数を使用した例を示します。

const mathOperations = {
  add: (a: number, b: number) => a + b,
  subtract: (a: number, b: number) => a - b,
  multiply: (a: number, b: number) => a * b,
}

console.log(mathOperations.add(5, 3)) // Output: 8
console.log(mathOperations.subtract(5, 3)) // Output: 2
console.log(mathOperations.multiply(5, 3)) // Output: 15

この例では、mathOperationsというオブジェクトには、加算、減算、乗算を行うための 3 つのアロー関数がプロパティとして格納されています。これらの関数はそれぞれ引数を 2 つ取り、計算結果を返します。オブジェクトのプロパティを通じてこれらの関数にアクセスし、計算を行うことができます。

このように、アロー関数をオブジェクトリテラルのプロパティとして使用することで、複数の関数をまとめて管理することができます。また、関数を値として持つオブジェクトを作ることで、関数の名前空間を管理することも可能となります。

Next.js で、オブジェクトを活用

Next.js と TypeScript を使って、オブジェクトを活用する具体的な例を見てみましょう。まず、Userというオブジェクトを受け取り、それを表示するコンポーネント User を作ります。

// components/User.tsx

type UserProps = {
  user: {
    name: string
    age: number
    greet: () => string
  }
}

const User = ({ user }: UserProps) => {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.age} years old</p>
      <button onClick={() => alert(user.greet())}>Greet</button>
    </div>
  )
}

export default User

このコンポーネント UserUserProps という型のプロパティを受け取ります。UserProps はオブジェクト user をプロパティとして持ちます。この usernameage というプロパティと、greetというメソッドを持つオブジェクトです。

そして、この User コンポーネントは、渡された user オブジェクトの nameage を表示します。さらに、greet メソッドを実行するボタンも用意しています。

greet: () => stringというコードは、TypeScript の型注釈における関数の型定義を表しています。

  • greet:はオブジェクトのプロパティ名を指定します。
  • () => stringは関数の型を定義します。この場合、引数を取らずに(())、文字列型(string)の値を返す関数を指定しています。

つまり、このコードは、「greetという名前のプロパティがあり、そのプロパティは引数を取らずに文字列を返す関数である」という情報を表現しています。

次に、この型注釈を用いた具体的なオブジェクトの例を見てみましょう:

const obj: { greet: () => string } = {
  greet: () => 'Hello, World!',
}

console.log(obj.greet()) // Output: "Hello, World!"

ここでは、objというオブジェクトが作成されています。このオブジェクトはgreetという名前のプロパティを持ち、そのプロパティは引数を取らずに文字列を返す関数であることが型注釈によって指定されています。具体的な関数としては、"Hello, World!"という文字列を返す関数が設定されています。

このコンポーネントを使って、オブジェクトを表示してみましょう。

// pages/index.tsx

import User from '../components/User'

const HomePage = () => {
  const user = {
    name: 'Taro',
    age: 25,
    greet: () => `Hello, I'm Taro.`,
  }

  return <User user={user} />
}

export default HomePage

この HomePage コンポーネントでは、User コンポーネントをインポートし、user オブジェクトを作成しています。その後、この user オブジェクトを User コンポーネントに渡しています。これにより、作成した User コンポーネントで user オブジェクトの内容を表示できます。

高度なオブジェクトの活用

ここでは、もう少し高度なオブジェクトの活用例を見てみましょう。具体的には、オブジェクトを使った状態管理です。React では、useState フックを使ってコンポーネントの状態を管理します。しかし、状態が複雑になった場合や、複数の状態を一緒に更新したい場合はどうすれば良いでしょうか。そのような場合には、オブジェクトを使って複数の状態をまとめて管理することができます。

// components/UserWithState.tsx

import { useState } from 'react'

type UserProps = {
  user: {
    name: string
    age: number
    greet: () => string
  }
}

const UserWithState = ({ user }: UserProps) => {
  const [userState, setUserState] = useState(user)

  const birthday = () => {
    setUserState((prevState) => ({ ...prevState, age: prevState.age + 1 }))
  }

  return (
    <div>
      <h2>{userState.name}</h2>
      <p>{userState.age} years old</p>
      <button onClick={birthday}>Birthday</button>
      <button onClick={() => alert(userState.greet())}>Greet</button>
    </div>
  )
}

export default UserWithState

この UserWithState コンポーネントでは、useState フックを使って user オブジェクトを状態として管理します。そして、「Birthday」ボタンを押すと、userStateage が 1 増えるようになります。このように、オブジェクトを使って複数の状態をまとめて管理することで、状態の更新をより直感的に、そして一緒に行うことができます。

以上、Next.js と TypeScript でオブジェクトを理解し、活用する方法について解説しました。オブジェクトは JavaScript の中心的な要素であり、Next.js と TypeScript でもその能力を最大限に活用することで、より高度なコードを書くことができます。

Next.js と TypeScript で、オブジェクトを理解する