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
GetStaticPropsContext
type that doesn't allowlocale
,locales
, ordefaultLocale
to be missing orundefined
.Derive a new
GetStaticProps
type 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: {}};
};