Applying specific data types to object properties for precise value identification in Typescript

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.

Answer №1

To enhance TypeScript's ability to determine the appropriate type for assignment, you can leverage type narrowing on a variable of type InputField.

var inputField: InputField;
switch (inputField.type) {
  case 'toggle':
    inputField.value; // is boolean
    inputField.value = 1; // Type 'number' cannot be assigned to type 'boolean'.
    break;

  case 'text':
    inputField.value; // is string
    break;

  case 'dropdown':
    inputField.selectValues; // is string[]
    break;

  default:
}

UPDATE: You can define the structure of userSettings as shown below...

interface ToggleInputField {
  title: string;
  type: 'toggle';
  value: boolean;
}
interface DropDownInputField {
  title: string;
  type: 'dropdown';
  value: string;
  selectValues: string[];
}
interface TextInputField {
  title: string;
  type: 'text';
  value: string;
}
type InputField = ToggleInputField | DropDownInputField | TextInputField;

const userSettings: Record<string, InputField> = {
  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',
  },
};

Assigning specific names to each input field type facilitates easy identification when hovering over a field, such as

userSettings.testDropdown.selectValues
, revealing its type as
DropDownInputField.selectValues: string[]
.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Having trouble retrieving values from the getEntry method in Contentful

How can I retrieve an entry from contentful using Contentful v10 with Node.js 18? I am having trouble accessing the value in getEntry(). interface Example { contentTypeId: 'item' fields:{ title: EntryFeildTypes.Text rate: EntryFeildType ...

What is a way to construct an object without resorting to casts or manually declaring variables for each field?

Click here for a hands-on example. Take a look at the code snippet below: export type BigType = { foo: number; bar?: number; baz: number; qux?: string[]; }; function BuildBigType(params: string[]) { // Here's what I'd like to do: ...

Creating a specialized feature for saving form data with React Admin

Within my react-admin application, I am faced with a scenario where I have a list of items accompanied by two separate buttons: "Create using email" and simply "Create". The "create" button utilizes the functionality provided by the data provider, which is ...

In Next.js, the switch button remains in the same state even after the page is refreshed

Is there a solution for this issue? I am currently developing a switch button for a configuration page. The problem arises when I toggle the switch from active to maintenance mode, save it successfully, but upon refreshing the page, the switch reverts back ...

The function 'transformArticles' is not recognized as a property of the object 'Article'

I'm encountering an issue with Typescript that I need help understanding. In my code, I have a route where I am importing a class called Article like this: import { Request, Response } from "express"; const appRoot = require("app-root-path"); import ...

The deletion by index feature seems to be malfunctioning in Angular

Is there a way to delete an item by its ID instead of just deleting the last element using this code? I want to create input fields with a delete button next to each one simultaneously. TS: public inputs: boolean[] = []; public addNew(): void { this ...

Evaluating file selection and uploading functionality within Spectron

Currently, I am faced with the challenge of writing a test for an electron GUI that includes a choose file dialog. Unfortunately, I do not have access to the inner workings of the GUI. Here is the code snippet I have written: await app.client.chooseFile( ...

Updating the value of a variable in a separate file with Node.js

I'm facing a business scenario that can be likened to a challenging situation. To simplify, here's the problem: File1.ts import * from 'something'; export const var1="value of var1"; //assume there is a variable 'x' ...

Top tips for handling HTML data in JSON format

I'm looking to fetch HTML content via JSON and I'm wondering if my current method is the most efficient... Here's a sample of what I'm doing: jsonRequest = [ { "id": "123", "template": '<div class=\"container\"&g ...

Can classes from an external package be imported into an Angular library within an Angular app using Openlayers?

I am currently developing an Angular library that configures an OpenLayers map as an Angular component. The component is functioning properly, but I need to access classes and constants from a package imported by the library, specifically OpenLayers. To w ...

Leverage TypeScript's enum feature by incorporating methods within each enum

In my TypeScript file, I have defined various events and interfaces: export type TSumanToString = () => string; export interface ISumanEvent { explanation: string, toString: TSumanToString } export interface ISumanEvents{ [key: string]: ...

Typescript's Type Specification

I am currently working with NextJs and Typescript and I am facing an issue. Whenever I include the "any" keyword in my code, it renders correctly. However, if I remove it, I encounter errors with post._id, post.title, and post.body. Challenge: Can someon ...

Angular input box with integrated datepicker icons displayed inside

Currently, I have an input field and a datepicker displayed in a row. However, I need to show an icon inside the input box instead. Here is my code: <div class="mb-2" style=" float: left;" class="example-full-width" class= ...

Guide on linking a trust policy to an IAM role through CDK

{ "Version": "2008-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "AWS": [ ...

Generate a data type automatically based on an Array

Imagine having an imaginary api that provides color values based on user selections. Consider the following arrays with string values: const Colors1 = ['red', 'blue', 'purple']; const Colors2 = ['blue', 'white& ...

Inspect the TypeScript typings within Svelte documents directly from the terminal

When I run tsc --noemit, it successfully checks for type errors in the codebase. However, I have encountered an issue where it does not seem to check .svelte files. Is there a way to enable this functionality? I can see the type errors in .svelte files wh ...

The recommended filename in Playwright within a Docker environment is incorrectly configured and automatically defaults to "download."

Trying to use Playwright to download a file and set the filename using download.suggestedFilename(). Code snippet: const downloadPromise = page.waitForEvent('download', {timeout:100000}) await page.keyboard.down('Shift') await p ...

The outcome of spawning Node.js is as follows: Python3 is unable to open the file './test' due to the error message [Errno 2] indicating that the file or directory does not exist

I am currently trying to execute a basic python script named test.py using the child process in Node JS, however I keep receiving an error message stating python3: can't open file './test': [Errno 2] No such file or directory. Despite my eff ...

I'm having trouble configuring the header in my Node/Express route

Using Node and the Express framework for my backend, along with React for my frontend, all coded in Typescript. The elastic search client is responsible for fetching data on the backend, but I don't believe that's where the issue lies. I'm ...

The property 'enabled' is not a standard feature of the 'Node' type

Within the code snippet below, we have a definition for a type called Node: export type Node<T> = T extends ITreeNode ? T : never; export interface ITreeNode extends TreeNodeBase<ITreeNode> { enabled: boolean; } export abstract class Tre ...