I'm trying to map a theme of mine into a theme object of forms, something like this:
type Color1 = string & { __brand: "color1" }
type Color2 = string & { __brand: "color2" }
interface Theme {
colors: {
primary: Color1;
};
border: {
radius: number;
};
fonts: {
main: string;
};
components: {
container: {
padding: number
},
radioCheckbox: {
active: Color1;
};
};
}
which should be transformed to
interface ThemeForm {
colors: {
primary: FormField<Color2>;
};
border: {
radius: FormField<number>;
};
fonts: {
main: FormField<boolean>;
};
components: {
container: {
padding: FormField<number>
},
radioCheckbox: {
active: FormField<Color2>;
};
};
}
where
type FormTypes = string | number | boolean | undefined;
interface FormField<F extends FormTypes> {
value: F;
error: boolean;
errorMsg: string;
}
If my Theme
interface were flatter, I know the solution would look something like this:
interface FlatterTheme {
radius: number;
padding: number;
...
};
type FlatterFormTheme = {
[K in keyof FlatterTheme]: FormField<FlatterTheme[K]>
}
However, my Theme
interface is unfortunately is nested and with different depth levels, so mapping that type is a bit beyond me. Does anyone know how to do it, or if it really is possible to do in TS?
You could write a ToForm<T>
utility type to transform Theme
to ThemForm
like this:
type ToForm<T> =
T extends Color1 ? FormField<Color2> :
T extends string ? FormField<boolean> :
T extends FormTypes ? FormField<T> :
{ [K in keyof T]: ToForm<T[K]> }
This is a conditional type that maps:
Color1
to FormField<Color2>
,string
that isn't a Color1
to FormField<boolean>
,FormTypes
T
that isn't a string
to FormField<T>
(so number
becomes FormField<number>
), and crucially:ToForm
applied to it.Note that the order of the clauses is important here because your various cases aren't mutually exclusive. For example, if they were flipped around like T extends string ? FormField<boolean> : T extends Color1 ? FormField<Color2> : ⋯
then it would do the wrong thing because Color1 extends string
.
Let's test it out:
type ThemeForm = ToForm<Theme>
/*
type ThemeForm = {
colors: {
primary: FormField<Color2>;
};
border: {
radius: FormField<number>;
};
fonts: {
main: FormField<boolean>;
};
components: {
container: {
padding: FormField<number>;
};
radioCheckbox: {
active: FormField<Color2>;
};
};
}
*/
Looks good; it matches the desired ThemeForm
from the question.