Home > Software engineering >  How to overwrite arguments of a function type in TypeScript?
How to overwrite arguments of a function type in TypeScript?

Time:08-16

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:

  1. Derive a new GetStaticPropsContext type that doesn't allow locale, locales, or defaultLocale to be missing or undefined.

  2. Derive a new GetStaticProps type that uses our new context type.

  3. 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: {}};
};
  • Related