Using Typescript mapped types to pair enum values as keys with corresponding values from an interface

My preferences are represented as an enum of strings:

export enum PreferenceTypes {
    language = "language",
    unit = "unit",
}

To define the structure of an expected object, I can create an interface where the keys correspond to the values of the enum:

export type UnitTypes = "µg/m3" | "ppm" | "ppb";
export type LanguageTypes = "English" | "Spanish";

export interface PreferenceOptions {
    [PreferenceTypes.language]: {
        name: string;
        value: LanguageTypes;
    }[];
    [PreferenceTypes.unit]: {
        name: string;
        value: UnitTypes;
    }[];
}

Now, my goal is to generate a default preferences object based on user locale. The keys of this new object should be the locale, with each value being an object. Each object's keys should match the PreferenceTypes, and their values should align with the value type defined in PreferenceOptions. However, constructing such a type constraint has proven challenging:

PreferenceByLocale: {
    [key: string]: { [key in PreferenceTypes]?: string };
} = {
    /** Defaults for UK users */
    en: {
        language: "English",
        unit: "µg/m3",
    },
    /** Defaults for Spain users */
    es: {
        language: "Spanish",
        unit: "µg/m3",
    },
    /** Defaults for US users */
    us: {
        language: "English",
        unit: "ppm",
    },
};

I am uncertain how to specify that the value of these objects should actually be

{ [T extends key in PreferenceTypes]?: PreferenceOptions[T]['value'] }
without encountering TypeScript errors. It is unclear if what I am attempting is feasible or if my approach to typing is overly complex. Additionally, there should be an error detection mechanism to flag incorrect entries like:

PreferenceByLocale: {
    [key: string]: { [key in PreferenceTypes]?: string };
} = {
    /** Defaults for mars users */
    mrs: {
        // This should trigger an error since "Martian" is not included in LanguageTypes
        language: "Martian", 
        unit: "µg/m3",
    },
}

Is it possible to achieve such a validation in TypeScript?

Answer №1

Alright, I believe I have a clearer understanding of your requirements now. Here is the revised version:

export enum PreferenceTypes {
  language = "language",
  unit = "unit",
}

export type UnitTypes = "µg/m3" | "ppm" | "ppb";
export type LanguageTypes = "English" | "Spanish";

export interface PreferenceOptions {
  [PreferenceTypes.language]: LanguageTypes;
  [PreferenceTypes.unit]: UnitTypes;
}

export interface PreferenceByLocale {
  [key : string]: PreferenceOptions;
}

const PreferenceByLocale: PreferenceByLocale = {
  /** Defaults for UK users */
  en: {
    language: "English",
    unit: "µg/m3",
  },
  /** Defaults for Spain users */
  es: {
    language: "Spanish",
    unit: "µg/m3",
  },
  /** Defaults for US users */
  us: {
    language: "English",
    unit: "ppm",
  },
  mrs: {
    language: "Unkown",
    unit: "sxsx"
  }
};

console.log(PreferenceByLocale);

At this point, an error is being triggered for 'mrs':

https://i.sstatic.net/9wjEl.png

I assume this aligns with your intended goals. If you find the code confusing and need further clarification, feel free to ask.

Answer №2

One common mistake to avoid is this: When defining PreferenceOptions, make sure the value of each property is not an array. Take a close look at the code snippet provided:

export interface PreferenceOptions {
[PreferenceTypes.language]: {
    name: string;
    value: LanguageTypes;
};
[PreferenceTypes.unit]: {
    name: string;
    value: UnitTypes;
};
}

Remember, in TypeScript, adding [] at the end of a value denotes it as an array. Both of these declarations are equivalent:

let names : Array<string> = [];
let names : string[] = [];

To correct the issue, update your PreferenceOptions like this:

export interface PreferenceOptions {
[PreferenceTypes.language]: {
    name: string;
    value: LanguageTypes;
};
[PreferenceTypes.unit]: {
    name: string;
    value: UnitTypes;
};
}

After making that change, you can test it with the following code:

const PreferenceByLocale: {
[key: string]: { [key in PreferenceTypes]?: string };
} = {
/** Defaults for UK users */
en: {
    language: "English",
    unit: "µg/m3",
},
/** Defaults for Spain users */
es: {
    language: "Spanish",
    unit: "µg/m3",
},
/** Defaults for US users */
us: {
    language: "English",
    unit: "ppm",
},
};
console.log(PreferenceByLocale)

This will result in the following output:

{
   en: { language: 'English', unit: 'µg/m3' },
   es: { language: 'Spanish', unit: 'µg/m3' },
   us: { language: 'English', unit: 'ppm' }
}

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 difficulty accessing an element within ng-template during the unit test writing process with Jasmine

I am encountering an issue when trying to access a button inside an ng-container in my testing environment. Despite manually setting the value of the ngIf condition to true, the elements inside are not being rendered. Here is what I have attempted so far: ...

Transitioning to Vue 3 has introduced a change where prop types are now classified

During the process of migrating the application from Vue 2 to Vue 3 by following this guide: , I encountered an issue related to Props and their types. All props were marked as type unknown by the linter, even though no errors were shown in the template it ...

What steps can be taken to effectively build a test suite architecture using Jest?

After exploring all the available resources on the Jest documentation website, including various guides and examples, I have yet to find a solution to my specific question. I am in search of a methodology that would enable me to individually run test case ...

Ensure that all files with the extension ".ts" take precedence and are imported

I am facing an issue with my component as I have two files associated with it: app/components/SomeButton.ts app/components/SomeButton.tsx The .ts file contains most of the logic and code, while the .tsx file extends the .ts and only contains the ren ...

Ways to Halt observable.timer in Angular 2

As I work on Angular2's Component, I am currently implementing the following functions: export class MypageEditComponent { ngOnInit() { this.timer = Observable.timer(100, 100); this.timer.subscribe(t => { this.setFormData(); } ...

Is it possible to display Angular Material Slider after the label?

Searching through the Angular Material docks, I came across the Sliders feature. By default, the slider is displayed first, followed by its label like this: https://i.sstatic.net/C5LDj.png However, my goal is to have the text 'Auto Approve?' sh ...

An effective method for excluding null values with an Angular pipe

I am currently working on an Angular pipe that filters results based on user input. The problem I'm encountering is that some of the results do not have a value, resulting in this error message: Cannot read property 'toLocaleLowerCase' o ...

Looking to incorporate Functional Components in React using the package "@types/react" version "^18.0.17"? Learn how here!

With the removal of the children prop from React.FC type, what is the new approach for typing components? ...

Implementing pagination within an Angular 11 Mat-table with grouping feature

Encountering an interesting issue with MatTable pagination and grouping simultaneously. I have two components each with a Mat-table featuring Pagination+Grouping. ComponentOne functions smoothly without any issues. When choosing to display 5 elements pe ...

How can you loop through an array of objects in TypeScript without relying on the traditional forEach

Currently, I'm working on an array of objects with the following structure. [ { "matListParent": "CH", "dParent": "CUST1", "isAllSelected": true, "childItems&qu ...

Populating datasets with relative indexing

I am working on a code where I need to fill the datasets with the property isProjected set to 1. There are 3 datasets - lower estimate, projected, and upper estimate. The goal is to fill the Lower Estimate and Upper Estimate with a background color of rgba ...

"Ensure Playwright refreshes the page automatically following navigation when a specific status code is

I find myself in a dilemma where I require my functional browser tests to verify the status code of each page response, and if a 503 error is encountered, try to reload the page a certain number of times before declaring failure. Even though I have experi ...

Unable to resolve the Typescript module within a different file

I am in the process of transitioning my React app to TypeScript. Currently, everything is working fine. However, I encountered an issue after adding the TypeScript compiler and renaming files to .ts and .tsx extensions - it is now throwing a "module not fo ...

angular 6 personalized material icons with ligature assistance

Can I create my own custom material icons with ligature support? Currently, I use svgIcon to get Custom Icons, Is there a way to make custom icons that support ligatures? Here is my current code snippet: app.component.ts import { Component } from &ap ...

Modifying Data with MomentJS when Saving to Different Variable

After attempting to assign a moment to a new variable, I noticed that the value changes on its own without any modification from my end. Despite various attempts such as forcing the use of UTC and adjusting timezones, the value continues to change unexpec ...

What is the method to access an interface or type alias that has not been explicitly exported in TypeScript type definitions?

I am looking to create a new class that inherits from Vinyl. The constructor in the superclass takes a single parameter of type ConstructorOptions. export default class MarkupVinylFile extends Vinyl { public constructor(options: ConstructorOptions) { ...

When it comes to dealing with signature overload, the behavior of Record and Map may not align

This scenario may seem straightforward, but it's causing confusion. I have a function with an overloaded signature that can accept either a Record or a Map. However, even though I am passing a Map as an argument, TypeScript is treating it as a Record. ...

The Static Interface Binding in TypeScript

I have inquired about how to extend the static functionality of existing objects in JavaScript (using TypeScript). In all examples provided here, I am utilizing Object The code below showcases a polyfill definition for ECMAScript's Object.is function ...

Tips on adding TypeScript annotations to an already existing global function

I'm contemplating enhancing an existing project by incorporating TypeScript type annotations. Struggling to supply an external declaration file for a straightforward example: app.ts: /// <reference path="types.d.ts"/> function welcome (person ...

Fails to update the ngModel linked to the checkbox

<label class="checkiconImg bg-white"> <input type="checkbox" [(ngModel)]="quoteSupplierCover.isShowInComparisonTool" /> <span class="geekmark ShowInComparisonToolCheckBox" ...