Reputation: 4747
I'm trying to extend the following Next.js function type:
export type GetStaticProps<
P extends { [key: string]: any } = { [key: string]: any },
Q extends ParsedUrlQuery = ParsedUrlQuery,
D extends PreviewData = PreviewData
> = (
context: GetStaticPropsContext<Q, D>
) => Promise<GetStaticPropsResult<P>> | GetStaticPropsResult<P>
So that 3 properties on its context (GetStaticPropsContext
) and never undefined
, like this:
type UpdateContext<T> = T extends (context: infer Context extends GetStaticPropsContext) => infer Return
? (context: SelectiveRequiredNotUndefined<Context, "locale" | "locales" | "defaultLocale">) => Return
: never;
Now if I do this, I can use the new type where those context properties are never undefined
:
type MyGetStaticProps = UpdateContext<GetStaticProps>;
const getStaticProps: MyGetStaticProps = async (context) => {
// do stuff
}
The problem is that MyGetStaticProps
is no longer generic. I know I could always do this instead:
type MyProps = {
someField: string
};
export const getStaticProps: UpdateContext<GetStaticProps<MyProps>> = async (context) => {
// do stuff
}
But I was wondering instead if there would not be a way to infer the type parameter definitions instead to avoid the more verbose syntax?
Just to recap, I am looking for a way to have some sort of type MyGetStaticProps = UpdateContext<GetStaticProps>;
but that would be supporting MyGetStaticProps<MyProps>
(infer the type parameters of GetStaticProps
)
Here is a fully working Playground link of the example
Upvotes: 5
Views: 440
Reputation: 26307
Unfortunately TypeScript cannot currently represent these kinds of types (known as "higher kinded types"), see microsoft/TypeScript#1213.
You could try and implement them, like this blog post does or this comment shows, but it's clunky and not fully representative of what HKTs can do.
It's probably better to use an alternative, or try to rethink your problem or even prevent it in the first place by redesigning some parts of your code.
In short, you can't really take a type, modify it, then spit the type back out, like you can with JS functions:
// type GetStaticProps<...> = ...;
function GetStaticProps(...) { ... }
// type UpdateContext<Func ...> = ...;
function UpdateContext(Func) {
const NewFunc = (...args) => {
args[0].prop = "something else";
return Func(...args);
};
return NewFunc;
}
// type Updated = UpdateContext<GetStaticProps>;
const Updated = UpdateContext(GetStaticProps);
// type Result = Updated<...>; // but Updated can't be a generic, it's a result of another type.
const Result = Updated({ prop: "will change" });
Some HKT comments from jcalz (more in-depth with potential workarounds for you):
Upvotes: 1
Reputation: 42160
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:
GetStaticPropsContext
such that certain properties of the context
are required and non-nullable.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
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>
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.
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.
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; }
Upvotes: 1