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