Problem
It took me a while to wrap my head around what you are trying to achieve here, but I think I finally understand that your goals are:
- To refine the
GetStaticPropsContext
such that certain properties of the context
are required and non-nullable.
- To be able to
infer
the type of the returned props from the function, rather than defining them yourself.
Resolving #2 by itself -- without #1 -- is simple as Next.js exports an InferGetStaticPropsType<T>
utility type for this.
export type InferGetStaticPropsType<T> = T extends GetStaticProps<infer P, any>
? P
: T extends (
context?: GetStaticPropsContext<any>
) => Promise<GetStaticPropsResult<infer P>>
? P
: never
source
The core of your problem is that implementing #1 will make it such that this type no longer works. This is because a function which accepts broader arguments extends
a function which accepts a narrower refinement of those arguments, but not the other way around.
An arbitrary illustration of that principle:
// this is `true`
type A = ((arg: number | string) => any) extends ((arg: string) => any) ? true : false;
// this is `false`
type B = ((arg: number) => any) extends ((arg: number | string) => any) ? true : false;
If your getStaticProps
function can only accept a refined context
type then the InferGetStaticPropsType<T>
utility type will no longer work because your getStaticProps
no longer extends
the basic GetStaticProps
.
export const getStaticPropsA = async (context: GetStaticPropsContext) => {
return {
props: {
someField: 'yes'
}
}
};
// this is `{ someField: string; }`
type PropsA = InferGetStaticPropsType<typeof getStaticPropsA>
export const getStaticPropsB = async (context: SelectiveRequiredNotUndefined<GetStaticPropsContext, "locale" | "locales" | "defaultLocale">) => {
return {
props: {
someField: 'yes'
}
}
};
// this is `never`
type PropsB = InferGetStaticPropsType<typeof getStaticPropsB>
Solution
Since you want to infer the return type, it doesn't make sense to assign a type like GetStaticProps
to the entire function. You'll want to just assign a type to the arguments. You've already got that part done with your SelectiveRequiredNotUndefined
utility type.
What's missing is the ability to infer the return type. For that, we need to modify some of the core Next.js types.
Approach 1: Module Augmentation
The locale
, locales
, and defaultLocale
properties are marked as optional because not all apps will use internationalization. But if your app includes this routing information in the next.config.js file then it is safe to assume that these properties will always be present.
You can use TypeScript module augmentation to modify the types of the next
GetStaticPropsContext
type.
import { PreviewData } from 'next';
import { ParsedUrlQuery } from 'querystring';
declare module 'next' {
export type GetStaticPropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery> = {
params?: Q
preview?: boolean
previewData?: PreviewData
locale: string
locales: string[]
defaultLocale: string
}
}
This should be the easy solution, but sometimes it can be a pain to set up properly. I thought that we would not have to also override the other types which use this, such as GetStaticProps
and InferGetStaticPropsType
, but perhaps we do?
This playground has some errors, but it does get the correct types.
Approach 2: Local Types
If augmenting the module is too much of a headache to get working, you can continue what you were already trying which is to create your own modified copies of the Next.js types. Instead of importing the Next.js versions, you would import from our own local types file.
import { GetStaticPropsContext, GetStaticPropsResult } from 'next';
import { ParsedUrlQuery } from 'querystring';
export type MyGetStaticPropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery> =
GetStaticPropsContext<Q> & Required<Pick<GetStaticPropsContext, 'locale' | 'locales' | 'defaultLocale'>>;
export type MyGetStaticProps<
P extends { [key: string]: any } = { [key: string]: any },
Q extends ParsedUrlQuery = ParsedUrlQuery
> = (context: MyGetStaticPropsContext<Q>) => Promise<GetStaticPropsResult<P>>
export type MyInferGetStaticPropsType<T> = T extends MyGetStaticProps<infer P, any>
? P
: T extends (
context?: MyGetStaticPropsContext<any>
) => Promise<GetStaticPropsResult<infer P>>
? P
: never
// ==== Example use:
export const getStaticProps = async (context: MyGetStaticPropsContext) => {
context.locale
// ^? (property) locale: string
context.locales
// ^? (property) locales: string[]
context.defaultLocale
// ^? (property) defaultLocale: string
context.preview // As an example of a property that doesn't get modified
// ^? (property) preview?: boolean | undefined
return {
props: {
someField: 'yes'
}
}
};
type MyProps = MyInferGetStaticPropsType<typeof getStaticProps>
// ^? type MyProps = { someField: string; }
Typescript Playground link