Skip to content

Commit

Permalink
fix: InferGetServerSidePropsType and InferGetStaticPropsType (#40635)
Browse files Browse the repository at this point in the history
## 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>
  • Loading branch information
HaNdTriX and ijjk authored Sep 20, 2022
1 parent c2f48ea commit 3943b20
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 16 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
"eslint-plugin-react-hooks": "4.5.0",
"event-stream": "4.0.1",
"execa": "2.0.3",
"expect-type": "0.14.2",
"express": "4.17.0",
"faker": "5.5.3",
"faunadb": "2.6.1",
Expand Down
22 changes: 6 additions & 16 deletions packages/next/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,10 @@ export type GetStaticProps<
context: GetStaticPropsContext<Q, D>
) => Promise<GetStaticPropsResult<P>> | GetStaticPropsResult<P>

export type InferGetStaticPropsType<T> = T extends GetStaticProps<infer P, any>
? P
: T extends (
context?: GetStaticPropsContext<any>
) => Promise<GetStaticPropsResult<infer P>> | GetStaticPropsResult<infer P>
? P
: never
export type InferGetStaticPropsType<T extends (args: any) => any> = Extract<
Awaited<ReturnType<T>>,
{ props: any }
>['props']

export type GetStaticPathsContext = {
locales?: string[]
Expand Down Expand Up @@ -181,16 +178,9 @@ export type GetServerSideProps<
context: GetServerSidePropsContext<Q, D>
) => Promise<GetServerSidePropsResult<P>>

export type InferGetServerSidePropsType<T> = T extends GetServerSideProps<
infer P,
any
export type InferGetServerSidePropsType<T extends (args: any) => any> = Awaited<
Extract<Awaited<ReturnType<T>>, { props: any }>['props']
>
? P
: T extends (
context?: GetServerSidePropsContext<any>
) => Promise<GetServerSidePropsResult<infer P>>
? P
: never

declare global {
interface Crypto {
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 69 additions & 0 deletions test/unit/infer-get-server-side-props-type.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type {
InferGetServerSidePropsType,
GetServerSidePropsContext,
} from 'next'
import { expectTypeOf } from 'expect-type'

describe('InferGetServerSidePropsType', () => {
it('should work with sync functions', async () => {
function getServerSideProps(context: GetServerSidePropsContext) {
if (context.params?.notFound) {
return {
notFound: true,
}
}

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

type PageProps = InferGetServerSidePropsType<typeof getServerSideProps>

expectTypeOf<PageProps>().toEqualTypeOf<{ foo: string }>()
})

it('should work with async functions', async () => {
async function getServerSideProps(context: GetServerSidePropsContext) {
if (context.params?.notFound) {
return {
notFound: true,
}
}

if (context.params?.redirect) {
return {
redirect: {
destination: '/',
},
}
}

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

type PageProps = InferGetServerSidePropsType<typeof getServerSideProps>

expectTypeOf<PageProps>().toEqualTypeOf<{ foo: string }>()
})

it('should work with promised props', async () => {
async function getServerSideProps() {
return {
props: Promise.resolve({
foo: 'bar',
}),
}
}

type PageProps = InferGetServerSidePropsType<typeof getServerSideProps>

expectTypeOf<PageProps>().toEqualTypeOf<{ foo: string }>()
})
})
52 changes: 52 additions & 0 deletions test/unit/infer-get-static-props.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { InferGetStaticPropsType, GetStaticPropsContext } from 'next'
import { expectTypeOf } from 'expect-type'

describe('InferGetServerSidePropsType', () => {
it('should work with sync functions', async () => {
function getStaticProps(context: GetStaticPropsContext) {
if (context.params?.notFound) {
return {
notFound: true,
}
}

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

type PageProps = InferGetStaticPropsType<typeof getStaticProps>

expectTypeOf<PageProps>().toEqualTypeOf<{ foo: string }>()
})

it('should work with async functions', async () => {
async function getStaticProps(context: GetStaticPropsContext) {
if (context.params?.notFound) {
return {
notFound: true,
}
}

if (context.params?.redirect) {
return {
redirect: {
destination: '/',
},
}
}

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

type PageProps = InferGetStaticPropsType<typeof getStaticProps>

expectTypeOf<PageProps>().toEqualTypeOf<{ foo: string }>()
})
})

0 comments on commit 3943b20

Please sign in to comment.