Next.js と TypeScript で、ジェネリック型を分かりやすく解説

TwitterFacebookHatena

TL;DR

この記事では、TypeScript の重要な概念であるジェネリック型を Next.js の開発環境でどのように使うかについて解説します。ジェネリック型の基本的な理解から、具体的な Next.js での実装方法まで、手を動かしながら学べる内容になっています。

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

ジェネリック型とは?

ジェネリクスは、まるで「箱」のようなものだと考えてみてください。この箱は特別で、何を入れてもその形状に変わります。たとえば、リンゴを入れるとリンゴの形に、サッカーボールを入れるとボールの形に、おもちゃの恐竜を入れると恐竜の形に変わる不思議な箱です。

TypeScript のジェネリック型も同じように、何かを「入れる」ことでその型になる機能を持っています。これにより、同じ関数やクラスで様々な型を扱うことができます。

次の例を見てみましょう。

function returnSame<T>(arg: T): T {
  return arg
}

この returnSame という関数は、ジェネリック型 T を引数 arg に使っています。この T がまさに前述の「不思議な箱」です。これを使うと、どんな型でも引数に入れて、そのまま返すことができます。

例えば次のように使用できます。

let outputString = returnSame<string>('hello') // outputString は 'hello'
let outputNumber = returnSame<number>(100) // outputNumber は 100

ここでは T の部分に具体的な型を入れて呼び出しています。すると、その関数はその型に合わせて動作します。つまり、不思議な箱にリンゴを入れたらリンゴの形に、サッカーボールを入れたらボールの形になるように、ジェネリクスもまた、指定した型に合わせて動作します。

このように、ジェネリック型は様々な型を柔軟に扱うための、とても強力なツールなんですね。

さらにわかりやすい例をご紹介しましょう。

ジェネリック型を使ったシンプルな関数を考えてみましょう。それが「箱」を作る関数だと想像してみてください。この「箱」はとても特殊で、何でも入れることができ、箱の中身はそのまま保管されます。

function createBox<T>(item: T) {
  return { content: item }
}

上記の createBox 関数は、ジェネリック型 T を使用しています。この関数に何かを渡すと、それがそのまま箱の中身 content となります。そして、この content の型は入れたもの item の型になります。そのため、この関数はどんな型でも扱うことができます。

それではこの関数を使ってみましょう。

let boxOfNumber = createBox(10) // { content: 10 }
let boxOfString = createBox('Hello') // { content: "Hello" }

createBox に数値を渡すと、その数値を保管した箱が返されます。同様に、文字列を渡すと、その文字列を保管した箱が返されます。

このようにジェネリクスは、とてもフレキシブルで、あらゆる種類の「箱」を作ることができます。だから、それがなんでも受け入れる「不思議な箱」のようなものだと言われるんですね。

アロー関数を使用した場合も、ジェネリック型の使用方法は同様です。以下にアロー関数を使った例をご紹介します。

まずは先程の「箱を作る」関数をアロー関数に書き換えてみましょう。

const createBox = <T,>(item: T) => {
  return { content: item }
}

この createBox 関数はジェネリック型 T を使っています。引数 item の型は T となり、そのまま content として保管されます。

それではこのアロー関数を使ってみましょう。

let boxOfNumber = createBox(10) // { content: 10 }
let boxOfString = createBox('Hello') // { content: "Hello" }

createBox に数値を渡すと、その数値を保管した箱が返されます。同様に、文字列を渡すと、その文字列を保管した箱が返されます。

関数式でも、アロー関数でも、ジェネリック型の使い方は基本的に同じです。ただし、宣言的な関数とアロー関数では、this の挙動が異なることに注意が必要です。どちらを使用するかは、その関数がどのように使用されるかによります。

まとめると、ジェネリック型は、TypeScript の機能で、再利用可能なコードを作成するための強力なツールといえます。これは、型の一部をパラメータとして扱うことで、コードの再利用性を高め、型の安全性を保つことができます。

function identity<T>(arg: T): T {
  return arg
}

この関数は、どんな型の引数でも受け取ることができ、そのまま返すことができます。このように、ジェネリック型は型の一部をパラメータ化して、一般化された関数やクラスを作成することができます。

Next.js で、ジェネリック型を活用

それでは、Next.js の開発において、ジェネリック型をどのように活用できるか見ていきましょう。ここでは、フェッチしたデータを扱うためのカスタムフックを作成します。

// hooks/useFetch.ts
import { useState, useEffect } from 'react'

type UseFetch<T> = [T | null, boolean, Error | null]

function useFetch<T>(url: string): UseFetch<T> {
  const [data, setData] = useState<T | null>(null)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<Error | null>(null)

  useEffect(() => {
    const fetchData = async () => {
      setLoading(true)
      try {
        const response = await fetch(url)
        const data = await response.json()
        setData(data)
      } catch (error) {
        setError(error)
      } finally {
        setLoading(false)
      }
    }
    fetchData()
  }, [url])

  return [data, loading, error]
}

export default useFetch

このカスタムフックは、任意の型 T のデータをフェッチし、そのデータとローディング状態、エラーを配列として返すものです。ここで T はジェネリック型で、使用時に具体的な型を指定することで、その型のデータを扱うことができます。

ジェネリック型を活用

以上で示したカスタムフックを、実際のコンポーネントで使ってみましょう。今回は、次のような型のデータを取得する API を想定します。

type UserData = {
  id: string
  name: string
  email: string
}

そして、この型のデータを取得するコンポーネントを以下のように作成します。

// components/User.tsx
import useFetch from '../hooks/useFetch'

type UserData = {
  id: string
  name: string
  email: string
}

const User = () => {
  const [user, loading, error] = useFetch<UserData>('/api/user')

  if (loading) {
    return <div>Loading...</div>
  }

  if (error) {
    return <div>Error: {error.message}</div>
  }

  return (
    <div>
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
    </div>
  )
}

export default User

ここで useFetch<UserData> としてジェネリック型を指定することで、user の型が UserData になります。これにより、user のプロパティに安全にアクセスすることができます。

Emotion で実装

それでは最後に、Emotion を使ってスタイルを適用してみましょう。Emotion もジェネリック型を活用することで、コンポーネントのプロパティに型安全性を提供します。

// components/StyledUser.tsx
import { css } from '@emotion/react'
import useFetch from '../hooks/useFetch'

type UserData = {
  id: string
  name: string
  email: string
}

type Props = {
  color: string
}

const StyledUser = ({ color }: Props) => {
  const [user, loading, error] = useFetch<UserData>('/api/user')

  if (loading) {
    return <div>Loading...</div>
  }

  if (error) {
    return <div>Error: {error.message}</div>
  }

  return (
    <div
      css={css`
        color: ${color};
      `}
    >
      <h1>{user?.name}</h1>
      <p>{user?.email}</p>
    </div>
  )
}

export default StyledUser

ここで、Props 型の color プロパティを受け取り、それを Emotion の css 関数で使用しています。この css 関数は、テンプレートリテラル内でジェネリクスを活用することで、スタイルプロパティに型安全性を提供します。

Next.js と TypeScript で、ジェネリック型を分かりやすく解説