0

I have two theme objects:

const lightMode = {
  background: "white",
  text: {
    primary: "dark",
    secondary: "darkgrey"
  },
} as const

const darkMode = {
  background: "black",
  text: {
    primary: "white",
  },
} as const

I want to get a type error if the lightMode object (default theme, which everyone will modify first) does not have the same shape as the darkMode object.

This will help people remember to update darkMode with some color values, if they add some new theme color to lightMode.

Devin Rhode
  • 23,026
  • 8
  • 58
  • 72

2 Answers2

1

You're overthinking this.

What you have a type that both object must implement to be correct. Like most things in Typescript, defining good data types up front will make things nice for you in the long run.

Make a type like:

type UITheme = {
    background: string,
    text: {
        primary: string
        secondary: string
    }
}

And now use it to make sure your objects are made properly.

const lightMode: UITheme = {
  background: "white",
  text: {
    primary: "dark",
    secondary: "darkgrey"
  },
} as const

const darkMode: UITheme = {
  background: "black",
  text: {
    primary: "white",
  },
} as const
// Property 'secondary' is missing in type
//   '{ readonly primary: "white"; }'
// but required in type
//   '{ primary: string; secondary: string; }'.

See playground


Or if you need the string literal types inferred, then use a generic function to create the objects and enforce the types.

type UITheme = {
    background: string,
    text: {
        primary: string
        secondary: string
    }
}

const createUIMode = <T extends UITheme>(theme: T) => theme

const lightMode = createUIMode({
  background: "white",
  text: {
    primary: "dark",
    secondary: "darkgrey"
  },
} as const)

const darkMode = createUIMode({
  background: "black",
  text: {
    primary: "white",
  },
} as const)
// error

See playground

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • Yeah, if I can avoid maintaining an extra schema, my actual themes are 25 lines long each, I'd like to avoid going from 50 to 75 lines if possible – Devin Rhode Aug 25 '22 at 13:47
  • MUI defines a `Theme` type, I can have light/dark theme as `UITheme extends Theme` generics... but then I still have to check that light theme and dark theme have the same "shape" (all the same customizations) – Devin Rhode Aug 25 '22 at 13:55
  • Ok, so the way MUI works, your theme object can really be basically anything. For example, we're defining a `theme.palette.text.inversePrimary`, maybe there's something intrinsically off about doing that, but we are for the moment, so doing some sort of `UITheme extends Theme` doesn't really do much for us. – Devin Rhode Aug 25 '22 at 20:44
  • If it can be anything, then MUI doesn't care that you made your own type to enforce consistency for your own themes. – Alex Wayne Aug 25 '22 at 21:35
  • Yeah, so MUI really should support something like `export const Chip = applyTheme(MUIChip, customThemeOptions)` – Devin Rhode Aug 26 '22 at 18:17
0

Here's my first attempt:

type NormalizeThemeConstType<someTheme extends object> = Writable<
  Schema<someTheme, string>
>;

const testAssignment: NormalizeThemeConstType<typeof darkMode> =
  lightTheme as NormalizeThemeConstType<typeof lightMode>;

The type errors seem crazy at first, but as is usually the case, looking at the end of the error revealed, one of our themes is missing a property from the other!

Devin Rhode
  • 23,026
  • 8
  • 58
  • 72
  • If you remove const, you can just use `const darkMode: typeof lightMode = {...}`. Or you must preserve string literal types? – KiraLT Aug 25 '22 at 05:40
  • I would really like preserving the string literals if possible – Devin Rhode Aug 25 '22 at 13:49
  • Related: https://stackoverflow.com/questions/49723173/merge-two-interfaces – Devin Rhode Aug 25 '22 at 20:47
  • I do suspect a dev-only runtime check would be better: https://stackoverflow.com/questions/14368596/how-can-i-check-that-two-objects-have-the-same-set-of-property-names – Devin Rhode Aug 25 '22 at 20:48