TypeScript とは一体なにか?

最近のフロントエンドでは、TypeScript がスタンダードになってきています。

TypeScript は、JavaScript のスーパーセットであり、静的型付けを導入することで、安全性と開発効率の向上を目指したプログラミング言語です。TypeScript のコードは、最終的に JavaScript にトランスパイルされるため、ブラウザや Node.js など、どこでも実行できます。

メリットは、以下の通りです。

  • 入力漏れのときに、エラーを出してくれる
  • コードのバグを早期に発見
  • コードの自動整形や構文チェックなどが簡単
  • 柔軟で保守性の高いコードを書くことができる
  • Visual Studio Code などのエディタで、型チェックができる
  • 開発環境のみ型チェックして、本番環境では不要でよい

デメリットは、少しだけ学習コストがかかるかな、という程度です。

Next.js と TypeScript の連携

後々、TypeScript で開発し直すことを考えると、最初から TypeScript で開発するのがよいでしょう。

Next.js は、React ベースのフレームワークなので、TypeScript との連携が容易にできるように設計されています。Next.js プロジェクトで TypeScript を使うためには、次の手順でセットアップを行います。

  1. TypeScript と関連パッケージのインストール
  2. tsconfig.json の作成
  3. .js ファイルを .tsx に変更

ただし、lib/ディレクトリは、.tsx ではなく .ts で作成します。

TypeScript と関連パッケージのインストール

まず、TypeScript と型定義ファイルをインストールします。プロジェクトルートで以下のコマンドを実行します。

npm install --save-dev typescript @types/react @types/node

tsconfig.json の作成

プロジェクトルートに tsconfig.json ファイルを作成し、以下の設定を追加します。

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

.js ファイルを .tsx に変更

.js 拡張子のファイルを .tsx に変更することで、TypeScript を使った開発ができるようになります。

型定義の基本

TypeScript の最大の特徴である型定義を活用することで、コードの安全性と開発効率が向上します。型定義の基本を見ていきましょう。

基本の型はこのようなものがあります。

  • string 型
  • number 型
  • boolean 型
  • Array(配列)型
  • null 型
  • undefined 型
  • any 型
  • 関数型
  • オブジェクト型

型注釈

変数や関数の引数、戻り値に型を明示的に指定することを「型注釈」と言います。例えば、変数や関数の型を指定する方法は以下の通りです。

// 変数の型注釈
const message: string = 'Hello TypeScript!'

// 関数の引数と戻り値の型注釈
function greet(name: string): string {
  return `Hello, ${name}!`
}

アノテーションとは、型が何であるかを明示的に TypeScript に伝えることです。アノテーション(annotation)とは、英語で「注釈」や「注記」という意味を持つ言葉です。

:

次の例は、変数、関数パラメータ、および関数戻り値の型アノテーションを示しています。

var num: number = 123
function identity(num: number): number {
  return num
}

Next.js では基本的にアロー関数を使います。

const hello = (引数名:): 戻り値の型 => `Hello`

書く引数の後ろに型を書きます。

// 返り値は引数リストの () の後に書くが、
function checkFlag(flag: boolean): string {
  console.log(flag)
  return 'check done'
}

// アロー関数も同様
const normalize = (input: string): string => {
  return input.toLowerCase()
}

変数の初期化を行うと自動的に型推論をします。

const age = 10
console.log(age.length) // エラーになる
// number 型に length プロパティはない

関数名を間違えて呼び出してもエラーになります。

const names = ['John', 'Jane']

names.forEach((name) => {
  // string型として扱われるので、関数名を間違えている呼び出しはエラーになる
  console.log(name.toUppercase()) // エラーになる 本来は、toUpperCase
})

オプショナルパラメータ

オプショナルを和訳すると「任意の」という意味です。プログラムでは、型を「任意」にすることができます。オブジェクトのプロパティを任意プロパティとして定義したい場合に使用します。TypeScript で、オブジェクトプロパティのオプショナルを型付けするには、プロパティ名の後ろに?を書きます。

function sayHello(greeting?: string): string {
  //...
}

インターフェイス

TypeScript では、オブジェクトの型を定義するために「インターフェイス」を使用します。インターフェイスを使ってオブジェクトの型を定義し、関数に適用する例は以下の通りです。

interface User {
  id: number
  name: string
  email: string
}

function getUserInfo(user: User): string {
  return `${user.name} (${user.email})`
}

ジェネリック

ジェネリックとは、クラスや関数において型を抽象化し外部から具体的な型を指定できる機能です。汎用的な型を作れます。Next.js ではクラスを使うことはないので、主に関数で使います。

TypeScript のジェネリックは、型の再利用性を向上させるために設計された機能です。これにより、関数やクラスにおいて、異なる型に対応する一般的な構造を定義できます。ジェネリックを使用すると、型の柔軟性を保ちながら、型安全性を維持できます。

Next.js での TypeScript のジェネリックの実装

Next.js アプリケーションで TypeScript のジェネリックを活用しましょう。ここでは、libディレクトリのutils.tsファイルにジェネリックを使用した関数を作成し、Next.js のページコンポーネントでその関数を利用します。

1. ジェネリック関数の作成

lib/utils.tsファイルに、ジェネリック関数getResponseを作成しましょう。この関数は、API からのデータ取得をシミュレートするために使用されます。関数は、ジェネリック型Tを受け取り、Promise を返します。

// lib/utils.ts
export async function getResponse<T>(url: string): Promise<T> {
  const response = await fetch(url)
  if (!response.ok) {
    throw new Error('An error occurred while fetching the data.')
  }
  const data: T = await response.json()
  return data
}

この関数は、異なる型のデータを取得する API エンドポイントに対応できるようになります。

2. Next.js ページコンポーネントでジェネリック関数を利用

pages/posts/[id].tsxファイルで、getResponse関数を利用して、個々の投稿データを取得しましょう。まず、Postインターフェースを定義して、投稿データの型を表現します。

// pages/posts/[id].tsx
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { getResponse } from '../lib/utils'

interface Post {
  id: number
  title: string
  body: string
}

export default function PostPage() {
  const router = useRouter()
  const { id } = router.query
  const [post, setPost] = useState<Post | null>(null)

  useEffect(() => {
    if (id) {
      getResponse<Post>(`https://jsonplaceholder.typicode.com/posts/${id}`)
        .then((data) => setPost(data))
        .catch((error) => console.error(error))
    }
  }, [id])

  return (
    <div>
      {post ? (
        <>
          <h1>{post.title}</h1>
          <p>{post.body}</p>
        </>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  )
}

Next.js と TypeScript の実践的な使い方

Next.js の各機能と TypeScript を組み合わせた実践的な使い方を解説します。

ページコンポーネントの型付け

Next.js のページコンポーネントは、通常の React コンポーネントと同様に型付けできます。以下は、Next.js のページコンポーネントに型注釈を適用した例です。

// pages/index.tsx
import type { NextPage } from 'next'

interface HomePageProps {
  title: string
}

const HomePage: NextPage<HomePageProps> = ({ title }) => {
  return <h1>{title}</h1>
}

export default HomePage

getStaticProps と getServerSideProps の型付け

Next.js のデータフェッチ機能である getStaticPropsgetServerSideProps も型付けすることができます。これにより、API から取得したデータの型安全性が向上します。

// pages/index.tsx
import type { NextPage, GetStaticProps } from 'next'

interface HomePageProps {
  title: string
}

const HomePage: NextPage<HomePageProps> = ({ title }) => {
  return <h1>{title}</h1>
}

export const getStaticProps: GetStaticProps<HomePageProps> = async () => {
  const title = 'Welcome to Next.js with TypeScript!'
  return {
    props: { title },
  }
}

export default HomePage

Next.js には、getStaticProps, getStaticPaths, getServerSideProps がありますが、それぞれ

GetStaticProps, GetStaticPaths, GetServerSideProps という型があり、これらを使うことで、型安全なコードを書くことができます。

import { GetStaticProps, GetStaticPaths, GetServerSideProps } from 'next'

export const getStaticProps: GetStaticProps = async (context) => {
  // ...
}

export const getStaticPaths: GetStaticPaths = async () => {
  // ...
}

export const getServerSideProps: GetServerSideProps = async (context) => {
  // ...
}

しかし、次の書き方 InferGetStaticPropsType で対応できるようになりました。

type Props = InferGetStaticPropsType<typeof getStaticProps>