Skip to content

Commit

Permalink
Add type inference for getStaticProps and getServerSideProps (#11842)
Browse files Browse the repository at this point in the history
This adds `InferredStaticProps` and `InferredServerSideProps` to the typings.

- [x] add types for type inference 
- [x] add explanation to docs
- [ ] tests - are there any?

![inferred-props](https://user-images.githubusercontent.com/56154253/79068041-24bcab00-7cc4-11ea-8397-ed1b95fbeca7.gif)

### What does it do:

As an alternative to declaring your Types manually with:
```typescript
type Props = {
  posts: Post[]
}

export const getStaticProps: GetStaticProps<Props> = () => ({
  posts: await fetchMyPosts(),
})

export const MyComponent(props: Props) =>(
 // ...
);
```

we can now also infer the prop types with
```typescript
export const getStaticProps = () => ({
  // given fetchMyPosts() returns type Post[]
  posts: await fetchMyPosts(),
})

export const MyComponent(props: InferredStaticProps<typeof getStaticProps>) =>(
 // props.posts will be of type Post[]
);

```

### help / review wanted
- [ ] I am no typescript expert. Although the solution works as intended for me, someone with more knowledge could probably improve the types. Any edge cases I missed?
- [ ] are there any tests I should modify/ add?
  • Loading branch information
manuschillerdev authored May 27, 2020
1 parent 37f4353 commit 9f1b4f5
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 7 deletions.
53 changes: 53 additions & 0 deletions docs/basic-features/data-fetching.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,34 @@ export const getStaticProps: GetStaticProps = async (context) => {
}
```

If you want to get inferred typings for your props, you can use `InferGetStaticPropsType<typeof getStaticProps>`, like this:

```tsx
import { InferGetStaticPropsType } from 'next'

type Post = {
author: string
content: string
}

export const getStaticProps = async () => {
const res = await fetch('https://.../posts')
const posts: Post[] = await res.json()

return {
props: {
posts,
},
}
}

function Blog({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
// will resolve posts to type Post[]
}

export default Blog
```

### Reading files: Use `process.cwd()`

Files can be read directly from the filesystem in `getStaticProps`.
Expand Down Expand Up @@ -452,6 +480,31 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
}
```

If you want to get inferred typings for your props, you can use `InferGetServerSidePropsType<typeof getServerSideProps>`, like this:

```tsx
import { InferGetServerSidePropsType } from 'next'

type Data = { ... }

export const getServerSideProps = async () => {
const res = await fetch('https://.../data')
const data: Data = await res.json()

return {
props: {
data,
},
}
}

function Page({ data }: InferGetServerSidePropsType<typeof getServerSideProps>) {
// will resolve posts to type Data
}

export default Page
```

### Technical details

#### Only runs on server-side
Expand Down
39 changes: 32 additions & 7 deletions packages/next/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,23 @@ export type GetStaticPropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery> = {
previewData?: any
}

export type GetStaticPropsResult<P> = {
props: P
unstable_revalidate?: number | boolean
}

export type GetStaticProps<
P extends { [key: string]: any } = { [key: string]: any },
Q extends ParsedUrlQuery = ParsedUrlQuery
> = (
ctx: GetStaticPropsContext<Q>
) => Promise<{
props: P
unstable_revalidate?: number | boolean
}>
> = (context: GetStaticPropsContext<Q>) => Promise<GetStaticPropsResult<P>>

export type InferGetStaticPropsType<T> = T extends GetStaticProps<infer P, any>
? P
: T extends (
context?: GetStaticPropsContext<any>
) => Promise<GetStaticPropsResult<infer P>>
? P
: never

export type GetStaticPaths<
P extends ParsedUrlQuery = ParsedUrlQuery
Expand All @@ -106,9 +114,26 @@ export type GetServerSidePropsContext<
previewData?: any
}

export type GetServerSidePropsResult<P> = {
props: P
}

export type GetServerSideProps<
P extends { [key: string]: any } = { [key: string]: any },
Q extends ParsedUrlQuery = ParsedUrlQuery
> = (context: GetServerSidePropsContext<Q>) => Promise<{ props: P }>
> = (
context: GetServerSidePropsContext<Q>
) => Promise<GetServerSidePropsResult<P>>

export type InferGetServerSidePropsType<T> = T extends GetServerSideProps<
infer P,
any
>
? P
: T extends (
context?: GetServerSidePropsContext<any>
) => Promise<GetServerSidePropsResult<infer P>>
? P
: never

export default next
46 changes: 46 additions & 0 deletions test/integration/typescript/pages/ssg/blog/[post].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {
InferGetStaticPropsType,
GetStaticPaths,
GetStaticPropsContext,
} from 'next'

type Post = {
author: string
content: string
}

export const getStaticPaths: GetStaticPaths = async () => {
return {
paths: [{ params: { post: '1' } }],
fallback: false,
}
}

export const getStaticProps = async (
ctx: GetStaticPropsContext<{ post: string }>
) => {
const posts: Post[] = [
{
author: 'Vercel',
content: 'hello wolrd',
},
]

return {
props: {
posts,
},
}
}

function Blog({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<>
{posts.map((post) => (
<div key={post.author}>{post.author}</div>
))}
</>
)
}

export default Blog
14 changes: 14 additions & 0 deletions test/integration/typescript/pages/ssg/blog/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { InferGetStaticPropsType } from 'next'

export const getStaticProps = async () => {
return {
props: { data: ['hello', 'world'] },
unstable_revalidate: false,
}
}

export default function Page({
data,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return <h1>{data.join(' ')}</h1>
}
32 changes: 32 additions & 0 deletions test/integration/typescript/pages/ssr/blog/[post].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { InferGetServerSidePropsType, GetServerSidePropsContext } from 'next'

type Post = {
author: string
content: string
}

export const getServerSideProps = async (
ctx: GetServerSidePropsContext<{ post: string }>
) => {
const res = await fetch(`https://.../posts/`)
const posts: Post[] = await res.json()
return {
props: {
posts,
},
}
}

function Blog({
posts,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<>
{posts.map((post) => (
<div key={post.author}>{post.author}</div>
))}
</>
)
}

export default Blog
1 change: 1 addition & 0 deletions test/integration/typescript/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export default function EvilPage(): JSX.Element {
it('should build the app', async () => {
const output = await nextBuild(appDir, [], { stdout: true })
expect(output.stdout).toMatch(/Compiled successfully/)
expect(output.code).toBe(0)
})

describe('should compile with different types', () => {
Expand Down

0 comments on commit 9f1b4f5

Please sign in to comment.