next.js icon indicating copy to clipboard operation
next.js copied to clipboard

Infer Types leading to `props: never`

Open lukebarton opened this issue 3 years ago • 25 comments

Describe the bug

export const getServerSideProps = async ({ params }) => {
  return { 
    props: { foo: "bar" } 
  }
};

export const Page = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => { ... }

causes props: never

however the following works fine:

export const getServerSideProps = async ({ params }: GetServerSidePropsContext) => {
  return { 
    props: { foo: "bar" } 
  }
};

export const Page = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => { ... }

as does:

export const getServerSideProps = async (ctx) => {
  const params = ctx.params;
  return { 
    props: { foo: "bar" } 
  }
};

export const Page = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => { ... }

lukebarton avatar Aug 05 '20 18:08 lukebarton

Next only checks this:

export type InferGetServerSidePropsType<T> = T extends GetServerSideProps<
  infer P,
  any
>
  ? P
  : T extends (
      context?: GetServerSidePropsContext<any>
    ) => Promise<GetServerSidePropsResult<infer P>>
  ? P
  : never

So if neither the function is typed nor the argument, I think it has no way of knowing if the function is actually of type GetServerSideProps. I supose changing the above to something like:

export type InferGetServerSidePropsType<T> = T extends GetServerSideProps<
  infer P,
  any
>
  ? P
  : T extends (
      context?: any
    ) => Promise<GetServerSidePropsResult<infer P>>
  ? P
  : never

would make it infer your type, because the conditon would hold true?

filipesmedeiros avatar Aug 07 '20 01:08 filipesmedeiros

I think you're right, however this one works, where the function isn't typed, and nor is the argument:

export const getServerSideProps = async (ctx) => {

lukebarton avatar Aug 25 '20 22:08 lukebarton

It also happens in cases like this:

export const getServerSideProps = async (ctx) => {
  const { userId } = ctx.params
  const user = await getUser(userId)

  if(user) {
    return {
      props: { user }
    }
  } else {
    return { notFound: true }
  }
};

JuanM04 avatar Nov 26 '20 16:11 JuanM04

This also doesn't work:

export const getServerSideProps: GetServerSideProps = async ({ params }) => {
  return { 
    props: { foo: "bar" } 
  }
};

export const Page = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => { ... }

or

export const getServerSideProps: GetServerSideProps = async ({ params }: GetServerSidePropsContext) => {
  return { 
    props: { foo: "bar" } 
  }
};

export const Page = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => { ... }

But removing the GetServerSideProps works:

export const getServerSideProps = async ({ params }: GetServerSidePropsContext) => {
  return { 
    props: { foo: "bar" } 
  }
};

export const Page = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => { ... }

NextJS: v10.0.1

For me, the magic was to make sure the getServerSideProps function args are explicitly typed using the GetServerSidePropsContext type. Otherwise, the page props are inferred as any type, e.g. user: any.

tgallacher avatar Jan 13 '21 17:01 tgallacher

I can confirm that @tgallacher bottom sample also works for me.

GeeWee avatar Feb 17 '21 12:02 GeeWee

This is a sneaky one, my string props were mistyped but that didn't even cause a problem with strict TS. It only was an issue when I had an object in the props and wanted to access a property.

thisismydesign avatar May 17 '21 13:05 thisismydesign

Here is my solution, key was to provide return type of getServerSideProps into GetServerSideProps type, here is example: https://gist.github.com/nenadfilipovic/f2dd9cb903da93a7d14ed1de6b3493b1

nenadfilipovic avatar Jun 27 '21 06:06 nenadfilipovic

Setting types like this works:

GetServerSideProps<{
    user: User
    posts: Post[]
}>

ezalivadnyi avatar Jul 12 '21 15:07 ezalivadnyi

I found out something interessing. Type inference via InferGetServerSidePropsType<typeof getServerSideProps> seems to be working as long as I only return props.

return { 
  props: { 
    user: { 
      firstName,
      lastName 
    } 
  } 
};

If i additionally return a redirect or a notFound conditionally type inference stops working for me.

return {
  redirect: {
    destination: '',
    permanent: false,
  },
};
return {
  notFound: true
};

Setting types like this works:

GetServerSideProps<{
    user: User
    posts: Post[]
}>

This also works for me, but it would be more comfortable if it would work without the extra type specification. If the type is created like this, you would not need to infer it, because you can directly use it in the component.

mkreuzmayr avatar Aug 17 '21 12:08 mkreuzmayr

It probably works in first case because return type is { [key: string]: any } That is default type for props object

nenadfilipovic avatar Aug 17 '21 12:08 nenadfilipovic

Having the same issue. Is there a proper solution? Not quite sure which one to choose in all the previous suggestions.

binajmen avatar Aug 18 '21 09:08 binajmen

@binajmen

The easiest solution imo would be by adding a PageProps type to GetServerSideProps generic. If there is a fix that the type infer starts working without the extra type specification you are already set up. And having your PageProps typed out is not a bad thing either.

type PageProps = {
  user: { 
    firstName: string,
    lastName: string
  };
};

export const getServerSideProps: GetServerSideProps<PageProps> = async (ctx) => {
  return { 
    props: { 
      user: { 
        firstName,
        lastName 
      } 
    } 
  };
};

export const Page = ({ user }: InferGetServerSidePropsType<typeof getServerSideProps>) => { ... }

mkreuzmayr avatar Aug 18 '21 12:08 mkreuzmayr

Hi @mkreuzmayr

Thanks, sounds like an acceptable solution! However it doesn't work with next-firebase-auth and I can't figure out how to combine PageProps with it:

import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'
import { AuthAction, useAuthUser, withAuthUser, withAuthUserTokenSSR } from 'next-firebase-auth'

function Page(props: InferGetServerSidePropsType<typeof getServerSideProps>) {
  const user = useAuthUser()

  return (
    <div>Message: {props.message} from {user.id}</div>
  )
}

export default withAuthUser<InferGetServerSidePropsType<typeof getServerSideProps>>({
  whenUnauthedBeforeInit: AuthAction.SHOW_LOADER,
  whenUnauthedAfterInit: AuthAction.REDIRECT_TO_LOGIN
})(Page)

export const getServerSideProps: GetServerSideProps<PageProps> = withAuthUserTokenSSR({
  whenUnauthed: AuthAction.REDIRECT_TO_LOGIN,
})(async () => {
  return { props: { message: "Hello" } }
})

TS error:

Type 'Promise<GetServerSidePropsResult<{ [key: string]: any; }>>' is not assignable to type 'GetServerSideProps<PageProps, ParsedUrlQuery>'.
  Type 'Promise<GetServerSidePropsResult<{ [key: string]: any; }>>' provides no match for the signature '(context: GetServerSidePropsContext<ParsedUrlQuery>): Promise<GetServerSidePropsResult<PageProps>>'.ts(2322)

binajmen avatar Aug 18 '21 15:08 binajmen

I've found a workaround - first of all - something broke with 11.1.x as could use InferGetServerSidePropsType<typeof getServerSideProps> even with notFound using the below.

Here's my hacky implementation that I just wrote:

utils/inferSSRProps.ts

/* eslint-disable @typescript-eslint/no-explicit-any */

type GetSSRResult<TProps> =
  //
  { props: TProps } | { redirect: any } | { notFound: true };

type GetSSRFn<TProps extends any> = (args: any) => Promise<GetSSRResult<TProps>>;

export type inferSSRProps<TFn extends GetSSRFn<any>> = TFn extends GetSSRFn<infer TProps>
  ? NonNullable<TProps>
  : never;

pages/somePage.tsx

import { inferSSRProps } from '../utils/inferSSRProps'
import { GetServerSidePropsContext } from "next";
import prisma from "@lib/prisma";

export default MyPage(props: inferSSRProps<typeof getServerSideProps>) {
  // ...
}

export const getServerSideProps = async (context: GetServerSidePropsContext) => {
  const post = await prisma.post.findFirst({
    where: {
      username: (context.query.slug as string).toLowerCase(),
    },
    select: {
      id: true,
      // ...
    },
  });
  if (!post) {
    return {
      notFound: true,
    } as const; // <-- important, this needs to be `as const`
  }
  return {
    props: {
     post,
    },
  };
}

KATT avatar Sep 02 '21 13:09 KATT

Hey @KATT, thanks for your solution!

You do not need to cast { notFound: true } to const if you change your GetSSRResult notFound type to boolean.

utils/inferSSRProps.ts

/* eslint-disable @typescript-eslint/no-explicit-any */

type GetSSRResult<TProps> =
  { props: TProps } | { redirect: any } | { notFound: boolean }; // <-------

type GetSSRFn<TProps extends any> = (args: any) => Promise<GetSSRResult<TProps>>;

export type inferSSRProps<TFn extends GetSSRFn<any>> = TFn extends GetSSRFn<infer TProps>
  ? NonNullable<TProps>
  : never;

mkreuzmayr avatar Sep 03 '21 10:09 mkreuzmayr

I solved the issue by publishing my own infer type:

  • [x] works with notFound
  • [x] works with redirect
  • [x] universal works for getStaticProps as well as getServerSideProps
  • [x] no dependencies

Install

npm install infer-next-props-type --save-dev

Usage:

getStaticProps

import InferNextPropsType from 'infer-next-props-type'

export function getStaticProps() {
   return {
     props: { foo: 'bar' }
   }
}

export default function Page(props: InferNextPropsType<typeof getStaticProps>) {
  return ...
}

getServerSideProps

import InferNextPropsType from 'infer-next-props-type'

export function getServerSideProps() {
   return {
     props: { foo: 'bar' }
   }
}

export default function Page(props: InferNextPropsType<typeof getServerSideProps>) {
  return ...
}

https://www.npmjs.com/package/infer-next-props-type

HaNdTriX avatar Oct 24 '21 14:10 HaNdTriX

@HaNdTriX any reason for not updating the built-in type?

timneutkens avatar Oct 29 '21 10:10 timneutkens

Because my type still has some edge cases to cover. Will deprecate the module as soon as we found the perfect working type and push the changes upstream.

HaNdTriX avatar Oct 29 '21 10:10 HaNdTriX

Might be interesting https://github.com/microsoft/TypeScript/issues/38511

balazsorban44 avatar Dec 01 '21 02:12 balazsorban44

I found out something interessing. Type inference via InferGetServerSidePropsType<typeof getServerSideProps> seems to be working as long as I only return props.

return { 
  props: { 
    user: { 
      firstName,
      lastName 
    } 
  } 
};

If i additionally return a redirect or a notFound conditionally type inference stops working for me.

return {
  redirect: {
    destination: '',
    permanent: false,
  },
};
return {
  notFound: true
};

Setting types like this works:

GetServerSideProps<{
    user: User
    posts: Post[]
}>

This also works for me, but it would be more comfortable if it would work without the extra type specification. If the type is created like this, you would not need to infer it, because you can directly use it in the component.

returning an empty props in redirect worked for me:

return {
  redirect: {
    destination: '',
    permanent: false,
  },
  props: {}
}

Markyiptw avatar Dec 14 '21 01:12 Markyiptw

Thanks @Markyiptw for the hint, that helped me a lot in figuring out what was going on.

The disadvantage of that solution is that the props will all become optional, so that may not always be ideal.

I slapped together this custom type that seems to do work well when using getServerSideProps that may return a redirect:

export type CustomInferGetServerSidePropsType<T> = T extends (
  context?: any
) => Promise<{ props: infer P }>
  ? P
  : T extends (context?: any) => Promise<GetServerSidePropsResult<infer P>>
  ? P
  : never;

I'm sharing this because the other types shared in this issue didn't quite work for my cases.

aengl avatar Feb 03 '22 17:02 aengl

I found a best work around. remove the : GetStaticProps from const getStaticProps = async () => {...}

Then the const SomePage: NextPage<InferGetStaticPropsType<typeof getStaticProps>>= ({/* inferred types */}){} can work correctly.

Problem was as said here https://github.com/vercel/next.js/issues/32434#issuecomment-993013691. So all after adding the : GetStaticProps , the return type of getStaticProps would be extended to { [key: string]: any; } by TS because it includes the original type of {'foo': string}.

PabloLION avatar Jun 02 '22 07:06 PabloLION

Looks like this will hopefully be solved with Typescript 4.9!

https://twitter.com/leeerob/status/1563540593003106306

image

flybayer avatar Aug 27 '22 17:08 flybayer

I was playing around with the new satisfies keyword and I can certainly see it helping.

We’ll still need to cast the notFound and redirects returns as consts.

I’m also of the opinion that we should expect props to be able to return null unless a type guard is in place, which I’ve also included in the below example.

Link to ts playground

andparsons avatar Aug 27 '22 23:08 andparsons

I have created a PR to solve this issue^^

HaNdTriX avatar Sep 20 '22 09:09 HaNdTriX

This issue has been fixed by #40635

HaNdTriX avatar Oct 03 '22 11:10 HaNdTriX

Closing per above

ijjk avatar Oct 03 '22 17:10 ijjk

This closed issue has been automatically locked because it had no new activity for a month. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

github-actions[bot] avatar Nov 03 '22 00:11 github-actions[bot]