I’m trying to create a strict mapping for a theme, but I want the keys of the mapping, not to be dynamic, but have optional values for the keys. Meaning the key type of "100 | AppBackground" can have one of those keys and the same value. Here are some of the things I have tried. I always want to allow strict key value pairs like red:color
.
type 300 = '300' | 'CardBackground';
export type ThemeMapping = {
[key: '100' | 'AppBackground']: Color;
[key in '200' | 'ContentBackground']: Color;
[key: 300 ]: Color;
['400' | 'InactiveBackground']: Color;
link: Color;
red: Color;
yellow: Color;
green: Color;
blue: Color;
}
I know for dynamic values you can also do {[key: string]: Color}
. I just want the key type to essentially be options rather than dynamic or just a regular string.
Please let me know if you need me to explain more.
2
Answers
You can combine multiple types to achieve this behavior.
Is this what you want?
TypeScript doesn’t natively support the concept of "an object with exactly one property from some set", so if you want to emulate it you’ll need to write something that works that way yourself. Here’s one approach:
The idea is that
ExactlyOneProp<T>
should result in a union type with one member for each property ofT
, where each union member should require on key and prohibit the others. TypeScript also can’t exactly prohibit a key; instead you can make an optional property whose value is the impossiblenever
type, so the only acceptable thing to do is leave the property out (or maybe set it toundefined
depending on compiler options). SoExactlyOneProp<{a: 0, b: 1, c: 2}>
should be equivalent to{a: 0, b?: never, c?: never} | {a?: never, b: 1, c?: never} | {a?: never, b?: never, c: 2}
.ExactlyOneProp<T>
works by mapping over the each property keyK
inT
, and making a new property of typePick<T, K> & {[P in Exclude<keyof T, K>]?: never}
(using thePick<T, K>
utility type to be an object with just a knownK
property) and then intersecting it with another mapped type with the keys inExclude<keyof T, K>
(using theExclude<X, Y>
utility type to filter outK
from the keys ofT
) with all optional properties (from the?
mapping modifier) of typenever
.That produces a fairly ugly type, so we will prettify it via
which essentially just collapses all object intersections to their equivalent single-object types (e.g.,
{a: 0} & {b: 1}
becomes{a: 0, b: 1}
.Let’s test that:
Looks good.
So now to make
ThemeMapping
the way you’d like, we need to intersect a bunch of different pieces, like this:That becomes a union of 2×2×2×2×1 = 16 members, each of which has 2+2+2+2+5 = 13 properties, so displaying it all at once is too much to do here. It looks like:
You can verify that it behaves as desired by trying out different possibilities:
As you wanted, you have to define exactly one of the
300
andCardBackground
propertes, and you’ll get an error if you try to define both or neither. And you can test the other properties yourself if you want.Playground link to code