An interesting type error arises if you use the standard advice for installing tRPC on an existing Next.js project that already has a layout applied.
Normally the advice for creating an app with a layout is to do this in your src/pages/_app.tsx:
// src/pages/_app.tsx
// ... imports go here
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function App({
Component,
pageProps: { session, ...pageProps },
}: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return getLayout(
<SessionProvider session={session}>
<GlobalProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</GlobalProvider>
</SessionProvider>
);
}
export default App
This works pretty well.
The docs for tRPC suggest wrapping that export:
// src/pages/_app.tsx
import type { AppType } from 'next/app';
import { trpc } from '../utils/trpc';
const MyApp: AppType = ({ Component, pageProps }) => {
return <Component {...pageProps} />;
};
export default trpc.withTRPC(MyApp);
Combining these seems simple:
// src/pages/_app.tsx
// ... imports go here
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode;
};
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function App({
Component,
pageProps: { session, ...pageProps },
}: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
return getLayout(
<SessionProvider session={session}>
<GlobalProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</GlobalProvider>
</SessionProvider>
);
}
export default trpc.withTRPC(App)
But this will throw a type error

Argument of type '({ Component, pageProps }: AppPropsWithLayout) => ReactNode' is not assignable to parameter of type 'NextComponentType<any, any, any>'.
Type '({ Component, pageProps }: AppPropsWithLayout) => ReactNode' is not assignable to type 'FunctionComponent<any> & { getInitialProps?(context: any): any; }'.
Type '({ Component, pageProps }: AppPropsWithLayout) => ReactNode' is not assignable to type 'FunctionComponent<any>'.
Type 'ReactNode' is not assignable to type 'ReactElement<any, any> | null'.
Type 'undefined' is not assignable to type 'ReactElement<any, any> | null'.ts(2345)
This is because getLayout returns a ReactNode, the tRPC wrapper expects NextComponentType, which means it expects a ReactElement to be returned?
What's the difference between a ReactNode and a ReactElement?
This StackOverflow post has a great explanation.
A
ReactElementis an object withtype,props, andkeyproperties:
...
AReactNodeis aReactElement,string,number,Iterable<ReactNode>,ReactPortal,boolean,null, orundefined:
So ReactElement can be cast to ReactNode, but not the other way around.
There is a simple fix. If you move the get layout call inside another React component, it will go back to being a ReactElement.
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;
};
function App({
Component,
pageProps: { session, ...pageProps },
}: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page);
const layout = getLayout(
<Layout>
<Component {...pageProps} />
</Layout>
);
return (
<SessionProvider session={session}>
<GlobalProvider>
{layout}
</GlobalProvider>
</SessionProvider>
);
}
export default trpc.withTRPC(App)
Lucky I was able to find this tutorial which mentions this exact footgun while I was working on adding tRPC to Penultimate Guitar.