フロントエンド開発環境のアップデート:Next.js 13、 React 18、 TypeScript 4.9 の導入

みなさんこんにちは。
アソビュー株式会社でフロントエンドエンジニアをしている櫻井と申します。

今回は、Next.js 12、React 17、TypeScript 4.7 で開発していたプロジェクトをNext.js 13、React 18 および TypeScript 4.9 にバージョンアップした際の対応について書きたいと思います。

目的と背景

昨年のアドベントカレンダーでご紹介した通り、現在、下記の記事にあるような大規模なシステム刷新プロジェクトが進行中です。それに伴い、開発期間が長期化しており、プロジェクト初期に導入したフレームワークやライブラリが最新版と比べて古くなっていることが課題となっていました。

もちろん、現状のままでも開発自体は続けられたのですが、中長期的に見ると、システムの完成間近になってから大規模なアップデートを行うと、影響範囲が広がるリスクがあります。

そこで、開発途中であっても影響範囲が限定的だと考えられる現時点で、アップデートを進めることが望ましいと判断し、早めに取り組むことにしました。

tech.asoview.co.jp

やったこと

それでは、アップデートに際して実施した手順について説明いたします。

環境関連の変更

Next.js や React のバージョンの変更

注意点として、ここでのバージョンは作業時点での最新版を使用しています。

アップデート前

"dependencies": {
  "next": "^12.2.0",
  "react": "^17.0.2",
  "react-dom": "^17.0.2",
  "typescript": "^4.4.4"
},
"devDependencies": {
  "@types/node": "^16.11.1",
  "@types/react": "^17.0.30",
  "@types/react-dom": "^17.0.9",
},

アップデート後

"dependencies": {
  "next": "^13.2.0",
  "react": "^18.2.0",
  "react-dom": "^18.2.0",
  "typescript": "^4.9.5"
},
"devDependencies": {
  "@types/node": "^18.11.9",
  "@types/react": "^18.0.28",
  "@types/react-dom": "^18.0.11",
},

lint 関連のバージョンの変更

アップデート前

"devDependencies": {
  "@next/eslint-plugin-next": "^12.2.2",
  "@typescript-eslint/eslint-plugin": "^4.26.1",
  "@typescript-eslint/parser": "^4.26.1",
  "eslint": "^7.6.0",
  "eslint-config-prettier": "^6.11.0",
  "prettier": "^2.0.5",
},

アップデート後

"devDependencies": {
  "@next/eslint-plugin-next": "^13.2.2",
  "@typescript-eslint/eslint-plugin": "^5.54.1",
  "@typescript-eslint/parser": "^5.54.1",
  "eslint": "^8.35.0",
  "eslint-config-prettier": "^8.7.0",
  "prettier": "^2.8.4",
},

next-transpile-modules の削除

Next.js v13.1 からは、next-transpile-modules が組み込みモジュールとして提供されるようになったため、package.json から削除します。

詳細については、下記の公式ドキュメントやGitHubを参照してください。

"devDependencies": {
  "next-transpile-modules": "^9.0.0",
}

nextjs.org

github.com

また、上記のGitHubに記載されている通り、外部モジュールを読み込む場合の設定方法が変更されているため、これに合わせて設定も変更します。

const withTM = require('next-transpile-modules')([
  '@mui/material',
  '@mui/system',
  '@mui/icons-material',
]);

module.exports = withBundleAnalyzer(
  withTM({
    pageExtensions: ['page.root.tsx'],
    webpack: (config) => {
      config.resolve.alias = {
  :

以下のように、transpilePackagesプロパティにリスト形式で設定します。

module.exports = withBundleAnalyzer({
  transpilePackages: [
    '@mui/material',
    '@mui/system',
    '@mui/icons-material',
  ],
  pageExtensions: ['page.root.tsx'],
  webpack: (config) => {
    config.resolve.alias = {
:

React 18 へのアップデートに伴う対応

次に、React 18 へのアップデートに伴うコードの修正を行います。

React v18 から children を明示的に定義する必要があるという変更が入りました。これまで 17 の環境では暗黙的に使用されていたため、該当箇所を修正します。

以前は Props に children を明示的に渡さずに参照できていたのですが、18 からは以下のように明示的に children ?: ReactNode; と定義する必要があります。以下は MenuItem というコンポーネントの例です。 このように Props に children を定義することで、<div>{children}</div>; のようにコンポーネントを定義できます。

import type { FC, ReactNode } from 'react';

type Props = {
  children?: ReactNode; // 追加
  onClick?: () => void;
};

export const MenuItem: FC<Props> = (props) => {
  const { children, onClick } = props;
  return <div onClick={onClick}>{children}</div>;
};

react.dev

TypeScript 4.9 へのアップデートに伴う対応

最後に、TypeScript 4.9 の機能を活用した対応について説明します。 先日、TypeScript 5.0 が発表されましたが、本作業時点では最新版だった 4.9 にアップデートしました。

satisfies operator

TypeScript 4.9 では、待ちに待った(?) satisfies operator という機能が実装されました。

公式ドキュメントによると、

The new satisfies operator lets us validate that the type of an expression matches some type, without changing the resulting type of that expression. As an example, we could use satisfies to validate that all the properties of palette are compatible with string | number[]:

とありますね。 satisfies operator を使うことによって、式の結果の型を変えずに、式の型がある型に一致するかどうかを検証してくれます。

devblogs.microsoft.com

以下の例を見てみましょう。

export const ticketTypes = {
  unspecified: '未指定',
  normal: '一般販売',
  precedence: '先行販売',
  lottery: '抽選販売',
}

このような形で、ticket のタイプがオブジェクトで定義されていると仮定しましょう。 このオブジェクトはコンポーネント内で ticketTypes.normal のような形で文字列を出力する際に参照されます。 この状態で型推論結果を見てみると、全て string として推論されます。そして、console.log などで出力すると、normal: string となります。

ここで、下記のように satisfies operator を付与してみます。
例えば、API のレスポンスの型として 別に enum TicketTypeResponse が定義されていて、その型と特定の文字列をマッピングしたい場合、satisfies { [key in TicketTypeResponse]: TicketTypeStr } とすることで、key に TicketTypeResponse を指定しつつ、value に TicketTypeStr のいずれかの文字列が入ることを表現することができます。

enum TicketTypeResponse {
  unspecified = 'unspecified',
  normal = 'normal',
  precedence = 'precedence',
  lottery = 'lottery',
}

type TicketTypeStr = '未指定' | '一般販売' | '先行販売' | '抽選販売';

const ticketTypes = {
  unspecified: '未指定',
  normal: '一般販売',
  precedence: '先行販売',
  lottery: '抽選販売',
} satisfies { [key in TicketTypeResponse]: TicketTypeStr }

これで型推論結果を見てみると、先程は string として推論されていましたが、下記のようにそれぞれ特定の文字列の型になっています。同じように console.log で出力してみると、`normal: "一般販売" となっていることがわかります。

まとめ

以上のように、

  • フレームワークとライブラリのバージョンアップの過程
  • 新バージョンに伴う設定変更とコード修正
  • TypeScript 4.9 の新機能の紹介と活用例

といった内容でアップデートを実施しました。 途中で述べたように、TypeScript 5.0 のリリースもあったため、その点もキャッチアップしつつ、プロダクトの開発に取り入れていく予定です。

アソビューではそのような最新の技術を積極的にプロダクトに取り入れ、より良いプロダクトを世の中に届けられるよう一緒に挑戦していくエンジニアを募集しています。カジュアル面談もやっていますので、気になった方はエントリーのほどお願いいたします! www.asoview.com

アソビュー!の技術情報を発信する公式アカウントもありますのでぜひフォローお願いします! https://twitter.com/Asoview_dev