『実践TypeScript』第10章をNext.js 9で実習する

書籍『[実践TypeScript](https://book.mynavi.jp/ec/products/detail/id=104703)』では[Next.js](https://nextjs.org/) 8系を使った手順が解説されていますが、出版後すぐに[Next.js 9系がリリース](https://nextjs.org/blog/next-9)され、これによってTypeScriptサポートが強化されました。そこで、本記事では、Next.js 9を使って『実践TypeScript』を実習していきます。

## 環境構築

next.js 9、React. 16.9を使います。

```package.json
{
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "^9.0.6",
    "react": "^16.9.0",
    "react-dom": "^16.9.0"
  }
}
```

```pages/index.jsx
import React from 'react'
const Page = props => (
  <div>Welcome to next.js!</div>
)
export default Page
```

`yarn install` でパッケージインストール後、 `yarn dev` でnext.jsサーバを起動して、動作確認しておきましょう。

## TypeScript導入

TypeScriptのコンパイラと、Node.jsおよびReact.js用の型定義ファイルをインストールします。

```sh
yarn add -D typescript @types/node @types/react
```

なお、書籍のように `@zeit/next-typescript` をインストールする必要はありません。TypeScriptサポート機能はNext.jsに内蔵されるようになっています。

TypeScriptサポートを有効化するには、 `pages/index.jsx` を `pages/index.tsx` にリネームします。

ここで `yarn dev` を実行すると、TypeScriptコンパイラの設定ファイル（tsconfig.json）など、必要なファイルが自動生成されます。

なお、バージョン違いによって、書籍で `next` モジュールからimportしている型が利用できないことがあります。

たとえば、 `Next.NextFC` という型はありません。pages/index.tsx では、かわりに `NextPage` 型をつかいましょう。

```tsx:pages/index.tsx
import React from 'react'
import { NextPage } from 'next'
const Page: NextPage = props => (
  <div>Welcome to next.js!</div>
)
export default Page
```

## Custom Components

書籍と同じ。

```tsx:layouts/index.tsx
import React from "react"
import { Head, Main, NextScript } from "next/document"

export default () => (
  <html>
    <Head />
    <body>
      <Main />
      <NextScript />
    </body>
  </html>
)
```

`Next.NextFC` 型はないので、 `NextComponentType` 型を使う。

```tsx:components/index.tsx
import React from "react"
import { NextComponentType } from "next"

const Component : NextComponentType = () => (
  <div>Welcome to next.js!</div>
)
export default Component
```

`next/document` からexportされている型も微妙に違うので注意（NextDocumentContextではなくDocumentContext）。

```tsx:pages/_document.tsx
import React from "react"
import Document, { DocumentContext } from "next/document"
import DefaultLayout from "../layouts/index"

export default class extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const initialProps = await Document.getInitialProps(ctx)
    return { ...initialProps }
  }
  render() {
    return <DefaultLayout />
  }
}
```

変更点は2つ。

- Containerは[Deprecated](https://github.com/zeit/next.js/blob/master/errors/app-container-deprecated.md)なので削除
- NextAppContext => AppContext

```tsx:pages/_app.tsx
import React from "react"
import App, { AppContext } from "next/app"

export default class extends App {
  static async getInitialProps({ Component, ctx }: AppContext) {
    let pageProps = {}
    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx)
    }
    return { pageProps }
  }

  render() {
    const { Component, pageProps } = this.props
    return (
      <Component {...pageProps} />
    )
  }
}
```

NextContext => NextPageContext

```tsx:pages/_error.tsx
import React from "react"
import { NextPageContext } from "next"
import Head from "next/head"

type Props = {
  title: string
  errorCode: number
}

class Error extends React.Component<Props> {
  static async getInitialProps({res}: NextPageContext): Promise<Props> {
    return {
      title: `Error: ${res!.statusCode}`,
      errorCode: res!.statusCode,
    }
  }

  render() {
    return (
      <>
        <Head>
          <title>{this.props.title}</title>
        </Head>
        {this.props.errorCode}
      </>
    )
  }
}

export default Error
```



```tsx:pages/index.tsx
import React from "react"
import Head from "next/head"
import Component from "../components/index";

type Props = {
  title: string
}

class App extends React.Component<Props> {
  static async getInitialProps(): Promise<Props> {
    return { title: "Hello world" }
  }

  render() {
    return (
      <>
        <Head>
          <title>{this.props.title}</title>
        </Head>
        <Component />
      </>
    )
  }
}

export default App
```

## styled-components

書籍の通りの手順でOK。 `Next.NextFC` 型はないので `NextComponentType` で代用。

```tsx:components/index.tsx
import React from "react"
import { NextComponentType, NextPageContext } from "next"
import styled from "styled-components"

type Props = {
  className?: string
}

const Component : NextComponentType<NextPageContext, {}, Props> = props => (
  <div className={props.className}>Welcome to next.js!</div>
)

const StyledComponent = styled(Component)`
color: #f00;
`

export default StyledComponent
```

## Redux

Reduxの導入のところは書籍の通りでOK。next.jsとの結合の部分では、そのままでは動かないところがあります。

書籍ではshims-next.d.ts でnext組み込みのinterfaceを拡張しています。しかし、next 9では拡張は必要ありません。

かわりに、storeを持ったPropsを定義して、これをAppの型引数に渡します。これでApp.propsがstoreプロパティをもつとみなされます。

```tsx:pages/_app.tsx
import React from "react"
import { Provider } from "react-redux"
import App, { AppContext } from "next/app"
import withRedux from "next-redux-wrapper"
import { initStore, ReduxStoreInstance } from "../store"

type Props = {
  store: ReduxStoreInstance
}

export default withRedux(initStore)(
  class extends App<Props> {
    static async getInitialProps({ Component, ctx }: AppContext) {
      let pageProps = {}
      if (Component.getInitialProps) {
        pageProps = await Component.getInitialProps(ctx)
      }
      return { pageProps}
    }

    render() {
      const { Component, pageProps, store } = this.props
      return (
        <Provider store={store}>
          <Component {...pageProps} />
        </Provider>
      )
    }
  }
)
```

## Express

ほぼ書籍通りでOK。例によって、NextContext型は無くなってるので、NextPageContext型を拡張します。

```tsx:
type ExNextPageContext = NextPageContext & {
  req?: Express.Request,
  res?: Express.Response,
}
```

## 感想

Next 9はTypeScriptサポートが強化されたという話でしたが、実際その通りで、セットアップはより簡単に、型はより柔軟で使いやすく、といった感じですね。