vercel / next.js

The React Framework
https://nextjs.org
MIT License
127.12k stars 27.01k forks source link

Infer Types leading to `props: never` #15913

Closed lukebarton closed 2 years ago

lukebarton commented 4 years ago

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>) => { ... }
filipesmedeiros commented 4 years ago

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?

lukebarton commented 4 years ago

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) => {
JuanM04 commented 3 years ago

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 }
  }
};
tgallacher commented 3 years ago

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.

GeeWee commented 3 years ago

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

thisismydesign commented 3 years ago

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.

nenadfilipovic commented 3 years ago

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

varanakonda commented 3 years ago

Setting types like this works:

GetServerSideProps<{
    user: User
    posts: Post[]
}>
mkreuzmayr commented 3 years ago

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.

nenadfilipovic commented 3 years ago

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

binajmen commented 3 years ago

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

mkreuzmayr commented 3 years ago

@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>) => { ... }
binajmen commented 3 years ago

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)
KATT commented 3 years ago

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,
    },
  };
}
mkreuzmayr commented 3 years ago

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;
HaNdTriX commented 3 years ago

I solved the issue by publishing my own infer type:

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

timneutkens commented 3 years ago

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

HaNdTriX commented 3 years ago

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.

balazsorban44 commented 2 years ago

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

Markyiptw commented 2 years ago

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: {}
}
aengl commented 2 years ago

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.

PabloLION commented 2 years ago

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}.

flybayer commented 2 years ago

Looks like this will hopefully be solved with Typescript 4.9!

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

image
andparsons commented 2 years ago

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

HaNdTriX commented 2 years ago

I have created a PR to solve this issue^^

HaNdTriX commented 2 years ago

This issue has been fixed by #40635

ijjk commented 2 years ago

Closing per above

github-actions[bot] commented 2 years ago

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.