fix: InferGetServerSidePropsType and InferGetStaticPropsType (#40635)

## 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>
This commit is contained in:
Henrik Wenz 2022-09-21 00:25:01 +02:00 committed by GitHub
parent c2f48ea86d
commit 3943b20f55
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 137 additions and 16 deletions

View file

@ -135,6 +135,7 @@
"eslint-plugin-react-hooks": "4.5.0", "eslint-plugin-react-hooks": "4.5.0",
"event-stream": "4.0.1", "event-stream": "4.0.1",
"execa": "2.0.3", "execa": "2.0.3",
"expect-type": "0.14.2",
"express": "4.17.0", "express": "4.17.0",
"faker": "5.5.3", "faker": "5.5.3",
"faunadb": "2.6.1", "faunadb": "2.6.1",

View file

@ -128,13 +128,10 @@ export type GetStaticProps<
context: GetStaticPropsContext<Q, D> context: GetStaticPropsContext<Q, D>
) => Promise<GetStaticPropsResult<P>> | GetStaticPropsResult<P> ) => Promise<GetStaticPropsResult<P>> | GetStaticPropsResult<P>
export type InferGetStaticPropsType<T> = T extends GetStaticProps<infer P, any> export type InferGetStaticPropsType<T extends (args: any) => any> = Extract<
? P Awaited<ReturnType<T>>,
: T extends ( { props: any }
context?: GetStaticPropsContext<any> >['props']
) => Promise<GetStaticPropsResult<infer P>> | GetStaticPropsResult<infer P>
? P
: never
export type GetStaticPathsContext = { export type GetStaticPathsContext = {
locales?: string[] locales?: string[]
@ -181,16 +178,9 @@ export type GetServerSideProps<
context: GetServerSidePropsContext<Q, D> context: GetServerSidePropsContext<Q, D>
) => Promise<GetServerSidePropsResult<P>> ) => Promise<GetServerSidePropsResult<P>>
export type InferGetServerSidePropsType<T> = T extends GetServerSideProps< export type InferGetServerSidePropsType<T extends (args: any) => any> = Awaited<
infer P, Extract<Awaited<ReturnType<T>>, { props: any }>['props']
any
> >
? P
: T extends (
context?: GetServerSidePropsContext<any>
) => Promise<GetServerSidePropsResult<infer P>>
? P
: never
declare global { declare global {
interface Crypto { interface Crypto {

View file

@ -96,6 +96,7 @@ importers:
eslint-plugin-react-hooks: 4.5.0 eslint-plugin-react-hooks: 4.5.0
event-stream: 4.0.1 event-stream: 4.0.1
execa: 2.0.3 execa: 2.0.3
expect-type: 0.14.2
express: 4.17.0 express: 4.17.0
faker: 5.5.3 faker: 5.5.3
faunadb: 2.6.1 faunadb: 2.6.1
@ -251,6 +252,7 @@ importers:
eslint-plugin-react-hooks: 4.5.0_eslint@7.24.0 eslint-plugin-react-hooks: 4.5.0_eslint@7.24.0
event-stream: 4.0.1 event-stream: 4.0.1
execa: 2.0.3 execa: 2.0.3
expect-type: 0.14.2
express: 4.17.0 express: 4.17.0
faker: 5.5.3 faker: 5.5.3
faunadb: 2.6.1 faunadb: 2.6.1
@ -14088,6 +14090,13 @@ packages:
homedir-polyfill: 1.0.3 homedir-polyfill: 1.0.3
dev: true dev: true
/expect-type/0.14.2:
resolution:
{
integrity: sha512-ed3+tr5ujbIYXZ8Pl/VgIphwJQ0q5tBLGGdn7Zvwt1WyPBRX83xjT5pT77P/GkuQbctx0K2ZNSSan7eruJqTCQ==,
}
dev: true
/expect/26.6.2: /expect/26.6.2:
resolution: resolution:
{ {

View file

@ -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 }>()
})
})

View file

@ -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 }>()
})
})