Home > Software design >  How to overwrite properties of a type with generics in TypeScript?
How to overwrite properties of a type with generics in TypeScript?

Time:08-15

I'm building a Next.js app in TypesScript and one of the type is:

type GetStaticPropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery, D extends PreviewData = PreviewData> = {
    params?: Q | undefined;
    preview?: boolean | undefined;
    previewData?: D | undefined;
    locale?: string | undefined;
    locales?: string[] | undefined;
    defaultLocale?: string | undefined;
}

I want to change it so that locale, locales, and defaultLocale are never undefined to avoid writing as string and as string[] castings everywhere since in my application, locales are always defined.

I tried creating a type like this

type LocaleConfig = {
  locale: string
  defaultLocale: string
  locales: string[]
}

And then just using

export const getStaticProps: GetStaticProps = (
  context: GetStaticPropsContext & LocaleConfig
) { // do stuff }

But I am getting a Type 'string | undefined' is not assignable to type 'string'. error.

I also tried to create an interface extending GetStaticPropsContext but unless I misunderstood how this works, I will also have to copy over the generics like this (which is not ideal):

export interface NewGetStaticPropsContext<
  Q extends ParsedUrlQuery = ParsedUrlQuery,
  D extends PreviewData = PreviewData
> extends GetStaticPropsContext {
  params?: Q | undefined
  previewData?: D | undefined
  locale: string
  defaultLocale: string
  locales: string[]
}

CodePudding user response:

What very probably happens is that GetStaticProps type is actually what causes the error message:

Should it look like:

type GetStaticProps = (context: GetStaticPropsContext) => void;

...then it is the assignment into getStaticProps variable of a function that has some required argument properties that TS does not like: you can assign a more "permissive" function (e.g. one that accepts optional arguments, into a stricter one, i.e. that accepts required arguments), but not the reverse.

type GetStaticPropsContext<Q extends ParsedUrlQuery = ParsedUrlQuery, D extends PreviewData = PreviewData> = {
  params?: Q | undefined;
  preview?: boolean | undefined;
  previewData?: D | undefined;
  locale?: string | undefined;
  locales?: string[] | undefined;
  defaultLocale?: string | undefined;
}

type LocaleConfig = {
  locale: string
  defaultLocale: string
  locales: string[]
}

export const getStaticProps = (
  context: GetStaticPropsContext & LocaleConfig
) => { } // Okay

type GetStaticProps = (context: GetStaticPropsContext) => void

const getStaticProps2: GetStaticProps = getStaticProps;
// Type '(context: GetStaticPropsContext & LocaleConfig) => void' is not assignable to type 'GetStaticProps'.
// Type 'string | undefined' is not assignable to type 'string'.
// Type 'undefined' is not assignable to type 'string'.

Playground Link

By saying that getStaticProps2 function is of GetStaticProps type, it means that we could call it with an empty argument (all properties in GetStaticPropsContext are optional):

getStaticProps2({});

But if getStaticProps is assigned into it, some properties would become required...


As to transform a type so that some of its optional properties become required, indeed, using an intersection (&) is the appropriate technique.

You can even use a combination of Required and Pick utility types to catch typos and ensure the exact same types:

type LocaleConfig = Required<Pick<GetStaticPropsContext, "locale" | "defaultLocale" | "locales">>
//   ^? { locale: string; defaultLocale: string; locales: string []; }

export const getStaticProps = (
  context: GetStaticPropsContext & LocaleConfig
) => {
  // do stuff
  const locale = context.locale
  //    ^? string
  const preview = context.preview
  //    ^? boolean | undefined
}

getStaticProps({
  locale: "locale",
  defaultLocale: "",
  //locales: [] // Error if any of the Required properties is missing
});
// Property 'locales' is missing in type '{ locale: string; defaultLocale: string; }' but required in type 'Required<Pick<GetStaticPropsContext<ParsedUrlQuery, PreviewData>, "locale" | "defaultLocale" | "locales">>'.

Playground Link

  • Related