Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infer Types leading to props: never #15913

Closed
lukebarton opened this issue Aug 5, 2020 · 28 comments
Closed

Infer Types leading to props: never #15913

lukebarton opened this issue Aug 5, 2020 · 28 comments
Labels
good first issue Easy to fix issues, good for newcomers TypeScript Related to types with Next.js.
Milestone

Comments

@lukebarton
Copy link
Contributor

lukebarton commented Aug 5, 2020

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 lukebarton changed the title Infer Types don't work brilliantly Infer Types leading to props: never Aug 5, 2020
@Timer Timer added kind: bug good first issue Easy to fix issues, good for newcomers labels Aug 5, 2020
@filipesmedeiros
Copy link

filipesmedeiros commented Aug 7, 2020

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
Copy link
Contributor Author

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) => {

@Timer Timer added this to the backlog milestone Sep 7, 2020
@JuanM04
Copy link
Contributor

JuanM04 commented Nov 26, 2020

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
Copy link

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
Copy link

GeeWee commented Feb 17, 2021

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

@thisismydesign
Copy link

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
Copy link

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
Copy link

Setting types like this works:

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

@mkreuzmayr
Copy link

mkreuzmayr commented Aug 17, 2021

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
Copy link

nenadfilipovic commented Aug 17, 2021

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

@binajmen
Copy link

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

@mkreuzmayr
Copy link

@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
Copy link

binajmen commented Aug 18, 2021

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
Copy link
Contributor

KATT commented Sep 2, 2021

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
Copy link

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
Copy link
Contributor

HaNdTriX commented Oct 24, 2021

I solved the issue by publishing my own infer type:

  • works with notFound
  • works with redirect
  • universal works for getStaticProps as well as getServerSideProps
  • 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

@timneutkens
Copy link
Member

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

@HaNdTriX
Copy link
Contributor

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
Copy link
Member

Might be interesting microsoft/TypeScript#38511

@Markyiptw
Copy link

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
Copy link

aengl commented Feb 3, 2022

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
Copy link

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 #32434 (comment). 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
Copy link
Contributor

Looks like this will hopefully be solved with Typescript 4.9!

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

image

@andparsons
Copy link

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
Copy link
Contributor

I have created a PR to solve this issue^^

ijjk added a commit that referenced this issue Sep 20, 2022
## Problem

Currently the Next.js infer utility (`InferGetServerSidePropsType` and
`InferGetStaticPropsType`) types can lead to a wrong inferred types
(`never`). This happens if these functions return something different
than: `{props: {}}`.

**Example:** `getServerSideProps`

```typescript
export async function getServerSideProps({ query }: GetServerSidePropsContext) {
  if (query.foo) {
    return {
      notFound: true,
    }
  }

  return {
    props: { 
      foo: "bar"
    },
  }
}

type PageProps = InferGetServerSidePropsType<typeof getServerSideProps>
// => type PageProps = never
```

**Example:** `getStaticProps`

```typescript
import type { InferGetStaticPropsType, GetStaticPropsContext } from 'next'

export async function getStaticProps(context: GetStaticPropsContext) {
  if (context.params?.bar) {
    return {
      notFound: true,
    }
  }

  return {
    props: {
      foo: 'bar',
    },
  }
}

type PageProps = InferGetStaticPropsType<typeof getStaticProps>
// => type PageProps = never
```

This is because the first infer condition of the utility type is not
satified leading to a never result.

```typescript
export type InferGetServerSidePropsType<T> = T extends GetServerSideProps<
  infer P, // <- NOT SATISFIED
  any
>
  ? P
  : T extends (
      context?: GetServerSidePropsContext<any>
    ) => Promise<GetServerSidePropsResult<infer P>>
  ? P
  : never  // <- NOT SATISFIED
```

## Solution

I have experimented with different solutions ending with a much simpler
type, that is faster to execute, easier to read and universally usable
for both prop variations.

```typescript
/**
 * Flow:
 * - Make sure getStaticProps is a function
 * - Get its return type
 * - Extract the one that contains {props: any}
 * - Return the props
 */
export type InferGetStaticPropsType<T extends (args: any) => any> = Extract<
  Awaited<ReturnType<T>>,
  { props: any }
>['props']
```

## Bug

- [x] Related issues: fixes #36615, #15913,
https://twitter.com/leeerob/status/1563540593003106306
- [x] Type tests added

## Future thoughts

Since `InferGetStaticPropsType` and `InferGetServerSidePropsType` are
now the same, it's api could be merged into one utility type (e.g:
InferNextProps). I recommend doing this in a different PR.

## Additional info

I have tested this approach using the following [external
package](https://www.npmjs.com/package/infer-next-props-type)
(@timneutkens sorry for the late PR). Since about 12 Month I haven't
received any negative feedback (issues) regarding this approach.

Co-authored-by: JJ Kasper <jj@jjsweb.site>
@HaNdTriX
Copy link
Contributor

HaNdTriX commented Oct 3, 2022

This issue has been fixed by #40635

@ijjk
Copy link
Member

ijjk commented Oct 3, 2022

Closing per above

@ijjk ijjk closed this as completed Oct 3, 2022
@github-actions
Copy link
Contributor

github-actions bot commented Nov 3, 2022

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 github-actions bot locked as resolved and limited conversation to collaborators Nov 3, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
good first issue Easy to fix issues, good for newcomers TypeScript Related to types with Next.js.
Projects
None yet
Development

No branches or pull requests