I've been working on creating a dynamic settings menu in TypeScript using the following data:
const userSettings = {
testToggle: {
title: "Toggle me",
type: "toggle",
value: false,
},
testDropdown: {
title: "Test Dropdown",
type: "dropdown",
value: "Change me",
selectValues: ["Option 1", "Option 2", "Option 3"]
},
testText: {
title: "Test textbox",
type: "text",
value: "Change me",
}
This setup allows for convenient access to each user input field by using
userSettings.testToggle.value
The challenge I'm facing is defining the types for the userSettings object. I've made some progress with the following code:
export type InputFieldTypes = {
toggle: boolean,
text: string,
dropdown: string,
}
type ExtraProperties = Merge<{ [_ in keyof InputFieldTypes]: {} }, {
// Additional properties which will be added to the input field with the same key
dropdown: {
selectValues: string[],
},
}>
type FieldOptions<K, V extends Partial<Record<keyof K, any>>> = {
[P in keyof K]: {
title: string,
type: P,
value: K[P],
} & V[P]
}[keyof K]
export type InputField = FieldOptions<InputFieldTypes, ExtraProperties>;
This results in a union type:
type InputField = {
title: string;
type: "toggle";
value: boolean; } | {
title: string;
type: "text";
value: string; } | ({
title: string;
type: "dropdown";
value: string; } & {
selectValues: string[]; })
While this effectively restricts toggle values to booleans and requires dropdown fields to have selectValues, assigning { [key: string]: InputField }
to userSettings
is too broad. The issue arises when userSettings.testToggle.value
has the type boolean | string
, causing casting complications.
I attempted a function approach:
const asInputFieldObject = <T,>(et: { [K in keyof T]: InputField & T[K] }) => et;
const settings = asInputFieldObject(userSettings);
Although this function enforces that userSettings objects are InputFields and allows typed accesses like settings.darkMode.value
, it triggers an error:
Type 'number' is not assignable to type 'never'.
when trying to assign a number to the toggle field instead of a boolean. Is there a cleaner solution to this problem?
Edit:
To clarify, the crux of the issue lies in assigning a type to userSettings
. The goal is to ensure that userSettings.testToggle.value
is strictly a boolean
rather than a string | boolean
. While it's feasible to assign types to objects in such a manner, simply applying InputField doesn't resolve the problem, as the value
access retains a union type across all possible values.
asInputFieldObject
represents my most recent attempt at rectifying this by incorporating intersection types into the mapped type:
[K in keyof T]: InputField & T[K]
. This results in a type structure like:
const userSettings: {
darkMode: {
title: string;
type: "toggle";
value: boolean;
} & {
title: string;
type: "toggle";
value: false;
};
testDropdown: {
title: string;
type: "dropdown";
value: string;
} & {
selectValues: string[];
} & {
...;
}; }
Although this method works well in most scenarios, providing clear typings, incorrect type assignments to value
can trigger a not assignable to type 'never'
error due to the intersections.