I'm working on a Next.js app and this type exist to to defined getStaticProps methods in pages:
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>
The problem is that GetStaticPropsContext is defined as follows and is not generic:
export type GetStaticPropsContext<
Q extends ParsedUrlQuery = ParsedUrlQuery,
D extends PreviewData = PreviewData
> = {
params?: Q
preview?: boolean
previewData?: D
locale?: string
locales?: string[]
defaultLocale?: string
}
In my app, locale, locales and defaultLocale are never undefined
Is there a way to overwrite GetStaticPropsContext on GetStaticProps without completely copy/pasting GetStaticProps to a new type and changing GetStaticPropsContext with another type?
I'm just worried about maintainability if I have to copy everything and was wondering if there is a better way.
CodePudding user response:
To get a new type that you can use to create your function, we have these steps:
Derive a new
GetStaticPropsContexttype that doesn't allowlocale,locales, ordefaultLocaleto be missing orundefined.Derive a new
GetStaticPropstype that uses our new context type.Use the new props type instead of
GetStaticProps.
Parts 1 and 2 are reusable, and part 3 (using it) is just using the new type on the function.
Note that this is written for maximum flexibility and minimum assumptions, because you said you wanted to avoid copy and paste on the existing types. It's possible to make it a bit less verbose by making more assumptions. That said, the actual code isn't that long (see the end), it just takes a fair bit of explanation.
1. Derive a new GetStaticPropsContext type
You can make a stricter version of Required that also removes undefined from the properties of an object type:
type RequiredNotUndefined<T> = {
[Key in keyof T]-?: Exclude<T[Key], undefined>;
};
That uses a mapped type with a mapping modifier to remove the optionality, and the Exclude utility type to remove undefined from its type.
Then, since we only want to apply this to some of the properties in GetStaticPropsContext, we can use this type to split off just those properties to pass through the above:
type SelectiveRequiredNotUndefined<T, Keys extends keyof T> =
RequiredNotUndefined<Pick<T, Keys>> & Omit<T, Keys>;
We use Pick to apply RequiredNotUndefined only to the named properties in Keys, and then intersect those with the other properties (via Omit).
Then you can create your own context type that derives from GetStaticPropsContext, making the three properties you care about required and not undefined:
export type MyStaticContext = SelectiveRequiredNotUndefined<
GetStaticPropsContext,
"locale" | "locales" | "defaultLocale"
>;
So, that's the context part so far; here's an example of it working: Playground link
2. Derive a new GetStaticProps type
But we don't really want to do type MyStaticContext = ___; we want to update GetStaticProps's context parameter type instead.
We can do that by mapping the function type, and using our SelectiveRequiredNotUndefined type to modify the type of context:
type UpdateContext<T> = T extends (context: infer Context extends GetStaticPropsContext) => infer Return
? (context: SelectiveRequiredNotUndefined<Context, "locale" | "locales" | "defaultLocale">) => Return
: never;
That uses the powerful and underdocumented infer feature to let us get the context and return types from GetStaticProps; then we modify context's type.
Then we use that to create our own MyGetStaticProps:
type MyGetStaticProps = UpdateContext<GetStaticProps>;
3. Use the new props type instead of GetStaticProps
const getStaticProps: MyGetStaticProps = async (context) => {
// ...
};
All together
Here's that all together (on the playground):
// Stand-in types
type ParsedUrlQuery = { x: number; };
type PreviewData = { data: Record<string, any>; };
type GetStaticPropsResult<X> = {blah: X; };
export type GetStaticPropsContext<
Q extends ParsedUrlQuery = ParsedUrlQuery,
D extends PreviewData = PreviewData
> = {
params?: Q;
preview?: boolean;
previewData?: D;
locale?: string;
locales?: string[];
defaultLocale?: string;
};
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>
// ==== >>>One time<<< declarations that get reused
type RequiredNotUndefined<T> = {
[Key in keyof T]-?: Exclude<T[Key], undefined>;
};
type SelectiveRequiredNotUndefined<T, Keys extends keyof T> =
RequiredNotUndefined<Pick<T, Keys>> & Omit<T, Keys>;
type UpdateContext<T> = T extends (context: infer Context extends GetStaticPropsContext) => infer Return
? (context: SelectiveRequiredNotUndefined<Context, "locale" | "locales" | "defaultLocale">) => Return
: never;
type MyGetStaticProps = UpdateContext<GetStaticProps>;
// ^?
// ==== Example use:
const getStaticProps: MyGetStaticProps = async (context) => {
context.locale
// ^?
context.locales
// ^?
context.defaultLocale
// ^?
context.preview // As an example of a property that doesn't get modified
// ^?
return {blah: {}};
};
