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

InferGetServerSidePropsType doesn't work with conditional returns #36615

Closed
1 task done
anthonyalayo opened this issue May 2, 2022 · 11 comments · Fixed by #40635
Closed
1 task done

InferGetServerSidePropsType doesn't work with conditional returns #36615

anthonyalayo opened this issue May 2, 2022 · 11 comments · Fixed by #40635
Labels
bug Issue was opened via the bug report template. TypeScript Related to types with Next.js.

Comments

@anthonyalayo
Copy link

anthonyalayo commented May 2, 2022

Verify canary release

  • I verified that the issue exists in Next.js canary release

Provide environment information

❯ next info

Operating System:
  Platform: darwin
  Arch: x64
  Version: Darwin Kernel Version 20.6.0: Tue Feb 22 21:10:41 PST 2022; root:xnu-7195.141.26~1/RELEASE_X86_64
Binaries:
  Node: 17.9.0
  npm: 8.5.5
  Yarn: N/A
  pnpm: N/A
Relevant packages:
  next: 12.1.6-canary.16
  react: 18.0.0
  react-dom: 18.0.0

What browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

Describe the Bug

In the documentation here: https://nextjs.org/docs/api-reference/data-fetching/get-server-side-props#redirect

We are shown that we can return a redirect or a notFound instead of props. I have attempted to do that in TypeScript here:

export default function user({ user }: InferGetServerSidePropsType<typeof getServerSideProps>) {
    return (
        <div>{user}</div>
    );
}

export const getServerSideProps = async ({ req }: GetServerSidePropsContext) => {
    const userJwt = parseCookies({ req })['jwt']
    if (!userJwt) {
        return { redirect: { destination: '/login', permanent: false } }
    }

    const user = await prisma.users.findFirst()
    return { props: { user } }
}

If there is logic handling a conditional redirect, we don't get type inference. Here is a WebStorm screenshot showing that:
image

Once I remove the conditional logic:

export default function user({ user }: InferGetServerSidePropsType<typeof getServerSideProps>) {
    return (
        <div>{user?.email}</div>
    );
}

export const getServerSideProps = async ({ req }: GetServerSidePropsContext) => {
    const user = await prisma.users.findFirst()
    return { props: { user } }
}

Then type inference works again:
image

Expected Behavior

Type inference works with conditionals.

To Reproduce

One way of reproducing is copy/pasting what I provided in the bug description:

export default function user({ user }: InferGetServerSidePropsType<typeof getServerSideProps>) {
    return (
        <div>{user}</div>
    );
}

export const getServerSideProps = async ({ req }: GetServerSidePropsContext) => {
    const userJwt = parseCookies({ req })['jwt']
    if (!userJwt) {
        return { redirect: { destination: '/login', permanent: false } }
    }

    const user = await prisma.users.findFirst()
    return { props: { user } }
}

This however requires extra dependencies to be installed. With no dependencies, you can simply take the example from the docs and attempt to use type inference with it: https://nextjs.org/docs/api-reference/data-fetching/get-server-side-props#redirect

@anthonyalayo anthonyalayo added the bug Issue was opened via the bug report template. label May 2, 2022
@balazsorban44 balazsorban44 added the TypeScript Related to types with Next.js. label May 3, 2022
@GabenGar
Copy link
Contributor

GabenGar commented May 3, 2022

You have to use GetServerSideProps/GetStaticProps interface instead and pass the Props interface of your page component to its first generic for the props to be picked up by Infer. You will have to do it anyway once you need to have some sort of baseline props interface across all pages with added benefit the return type of your SSR function will also be typechecked.

@anthonyalayo
Copy link
Author

@GabenGar I just switched getServerSideProps to:

export const getServerSideProps: GetServerSideProps = async ({ req }) => {
    const userJwt = parseCookies({ req })['jwt']
    if (!userJwt) {
        return { redirect: { destination: '/login', permanent: false } }
    }

    const user = await prisma.users.findFirst()
    return { props: { user } }
}

and user is still being detected as any. Could you copy/paste the correct way of writing it? Perhaps I'm missing something?

@GabenGar
Copy link
Contributor

GabenGar commented May 3, 2022

Let's say you have interface PageProps { user: unknown } declared somewhere on the component file.
Then you write GetServerSideProps<PageProps> as the type for your getServerSideProps() function.

@anthonyalayo
Copy link
Author

I think I attempted what you mentioned, but no luck still. I also went back to the docs:
https://nextjs.org/docs/api-reference/data-fetching/get-server-side-props#getserversideprops-with-typescript

and I couldn't find a similar example either. I'm still new to TypeScript, perhaps you could copy/paste my example with the changes?

@GabenGar
Copy link
Contributor

GabenGar commented May 3, 2022

I have a page which uses it.
The lines of interest are 30 and 128.

@anthonyalayo
Copy link
Author

anthonyalayo commented May 3, 2022

That worked, thank you @GabenGar! Here it is completely, so people can reference it in the future:

import { prisma } from '../db'
import { users } from '@prisma/client'
import { GetServerSideProps, InferGetServerSidePropsType } from "next/types";
import { parseCookies } from "nookies";

export default function Users({ users }: InferGetServerSidePropsType<typeof getServerSideProps>) {
    return (
        <ul>
            {users.map(user => (
                <li key={user.id}>{user.email}</li>
            ))}
        </ul>
    );
}

interface IProps {
    users: users[]
}

export const getServerSideProps: GetServerSideProps<IProps> = async ({ req }) => {
    const userJwt = parseCookies({ req })['jwt']
    if (!userJwt) {
        return { redirect: { destination: '/login', permanent: false } }
    }

    const users = await prisma.users.findMany()
    return { props: { users } }
}

My mistake was that I did this:

interface IProps {
    props: {
        users: users[]
    }
}

@GabenGar, aren't we returning an object with everything inside a props object? Why does including that in the interface not work? I'm confused about that.

@GabenGar
Copy link
Contributor

GabenGar commented May 3, 2022

aren't we returning an object with everything inside a props object? Why does including that in the interface not work? I'm confused about that.

Because the generic for GetServerSideProps interface defines the shape of the props object, not the entire result from it.

@anthonyalayo
Copy link
Author

Thanks @GabenGar, that makes sense. I appreciate the help!

@balazsorban44, could this ticket track making the documentation here more clear?
https://nextjs.org/docs/api-reference/data-fetching/get-server-side-props#getserversideprops-with-typescript

I'm not a TypeScript expert, but I could imagine others getting confused by it as well.

@anthonyalayo
Copy link
Author

@balazsorban44 bumping this issue

@Haberkamp
Copy link

I'm not a TypeScript expert, but I could imagine others getting confused by it as well.

@anthonyalayo you're absolute right! I was so confused on why this did not work.

Updating the documentation would certainly help other people.

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>
@github-actions
Copy link
Contributor

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 Oct 21, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Issue was opened via the bug report template. TypeScript Related to types with Next.js.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants