Say I have an object defining my Typography components color and shade.
const theme = {
primary: [10, 20, 50, 100],
secondary: [20, 30, 80],
accent: [10, 20]
}
Typography should accept 2 props, color and shade.
Type definition for color can be: keyof typeof theme.
How do I specify the type of shade prop such that it only accepts the value within the array of the palette defined in the theme object? For instance, if color is accent, shade cannot be 30. It can only be 10 or 20.
Code snippet:
function Typography({
color,
shade,
children,
}: {
color: keyof typeof theme;
shade: ???;
children: React.ReactNode;
}) {
return <div>{children}</div>;
}
function Main() {
return (
<Typography
color="primary"
shade={}>
Some text
</Typography>
);
}
Codesandbox – https://codesandbox.io/s/conditional-prop-types-cwy49q
>Solution :
There are two possible approaches:
- Using generics
- Create a union of all possibilities (not recommended for big amount of values)
Either of the approaches will require a change in theme.
Currently, the type of the theme is Record<string, number[]> which is not suitable for us. We have to prevent the compiler from widening types to their primitives.
This can be done by using const assertion:
const theme = {
primary: [10, 20, 50, 100],
secondary: [20, 30, 80],
accent: [10, 20],
} as const;
The problem with const assertion is that we lose type checking and if you use Typescript >= 4.9 it can be fixed with satisfies operator:
const theme = {
primary: [10, 20, 50, 100],
secondary: [20, 30, 80],
accent: [10, 20],
} as const satisfies Record<string, readonly number[]>;
Since const assertion converts everything to readonly it is crucial to write readonly number[] in satisfies instead of just number[].
Let’s create a type for the theme:
// type Theme = {
// readonly primary: readonly [10, 20, 50, 100];
// readonly secondary: readonly [20, 30, 80];
// readonly accent: readonly [10, 20];
// }
type Theme = typeof theme;
Approach with generics:
Typography will accept a generic parameter constrained by the keyof Theme and by using it we will get the values for shade using indexed access:
function Typography<T extends keyof Theme>({
color,
shade,
children,
}: {
color: T;
shade: Theme[T][number];
children: React.ReactNode;
}) {
return <div>{children}</div>;
}
Approach with the union of possible values:
By using mapped types we will map through Theme and generate a union of {color: string, shade: number}:
type PossibleValuesUnion = {
[K in keyof Theme]: { color: K, shade: Theme[K][number] }
}[keyof Theme]
Usage:
function Typography({
color,
shade,
children,
}: PossibleValuesUnion & {
children: React.ReactNode;
}) {
return <div>{children}</div>;
}
Either of these should do the trick, personally, I would recommend the first approach if the theme will grow bigger.