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
関数は、テンプレートリテラル内でジェネリクスを活用することで、スタイルプロパティに型安全性を提供します。