Next.js Per-Page Layouts and TypeScript

Next.js allows developers to set up dynamic layouts per-page. Benefits and details of this approach can be read here. However, doing what is described there will generate some issues when we use TypeScript in strict mode.

What's wrong

Example code from the official documentation:

// pages/_app.tsx

export default function MyApp({ Component, pageProps }) {
  // Use the layout defined at the page level, if available
  const getLayout = Component.getLayout || ((page) => page)

  return getLayout(<Component {...pageProps} />)
}

Generates following errors:

Parameter 'page' implicitly has an 'any' type.
Property 'getLayout' does not exist on type 'NextComponentType<NextPageContext, any, {}>'.
  Property 'getLayout' does not exist on type 'ComponentClass<{}, any> & { getInitialProps?(context: NextPageContext): any; }'

Fix the first error

The first one we can easily fix, by importing a proper type for the page param:

import { ReactNode } from 'react';

Let's use it in our code:

// pages/_app.tsx

export default function MyApp({ Component, pageProps }) {
  // Use the layout defined at the page level, if available
  const getLayout = Component.getLayout || ((page: ReactNode) => page)

  return getLayout(<Component {...pageProps} />)
}

Great! The first error is gone.

Fix the second error

The second one is more complicated. What happens is that original type for Component doesn't include getLayout function. We need to declare a new types. Let's create a next.d.ts file somewhere in the project, with the following content:

// next.d.ts

import type {
  NextComponentType,
  NextPageContext,
  NextLayoutComponentType,
} from 'next';
import type { AppProps } from 'next/app';

declare module 'next' {
  type NextLayoutComponentType<P = {}> = NextComponentType<
    NextPageContext,
    any,
    P
  > & {
    getLayout?: (page: ReactNode) => ReactNode;
  };
}

declare module 'next/app' {
  type AppLayoutProps<P = {}> = AppProps & {
    Component: NextLayoutComponentType;
  };
}

It creates new types NextLayoutComponentType and AppLayoutProps that we can use in place of original types. Our initial code will need to be changed to this:

// pages/_app.tsx

import { AppContext, AppInitialProps, AppLayoutProps } from 'next/app';
import type { NextComponentType } from 'next';
import { ReactNode } from 'react';

const MyApp: NextComponentType<AppContext, AppInitialProps, AppLayoutProps> = ({
  Component,
  pageProps,
}: AppLayoutProps) => {
  const getLayout = Component.getLayout || ((page: ReactNode) => page);
  return getLayout(<Component {...pageProps} />);
};

export default MyApp;

Please note that we are using the custom type we've created - AppLayoutProps. It includes the other custom type for Component that now contains getLayout function.

This solution was based on ippo012/nextjs-starter project, in which author used a very similar approach.

22