Develop a mapping system using enums that ensures compiler errors are enforced

I am struggling to enforce a mapping on an object in TypeScript. My goal is to define a type or interface that maps from ANIMAL_PLACE to ANIMAL_TYPE. I want the type to ensure that any object created with these mappings includes both the ANIMAL_PLACE and ANIMAL_TYPE keys. However, I have only been able to achieve this halfway.

export enum ANIMAL_TYPE {
    dog = "dog",
    cat = "cat",
    fish = "fish",
    foo = "foo"
}

export enum ANIMAL_PLACE {
    europe = 'europe',
    africa = 'africa',
    asia = "asia"
}

// Despite my efforts, this type does not trigger a compiler error when ANIMAL_PLACE or ANIMAL_TYPE is missing
type AnimalPlaceToAnimalMapType = {
    [ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish]
    [ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog]
}

// Only ANIMAL_PLACE triggers a compiler error when missing from the record
const AnimalPlaceToAnimalMapRecord: Record<ANIMAL_PLACE, ANIMAL_TYPE[]> = {
    [ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish],
    [ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog],
};

Try out the code in this Playground

If I cannot achieve an error when either key is missing, is there a way to trigger a compiler error specifically if ANIMAL_TYPE is omitted instead of ANIMAL_PLACE...?

Answer №1

To ensure that two types are "the same" in TypeScript, you can create a utility type. Although this utility type actually checks for mutual assignability rather than strict sameness, it serves the purpose well:

type Same<T extends U, U extends V, V = T> = void;

While the utility type itself doesn't output anything significant, it sets constraints on the types T and U. So, when you use Same<T, U> with different types, a compiler error will be triggered for either T or U:

type T1 = Same<string, string>; // okay
type T2 = Same<string, number>; // error!
// Type 'string' does not satisfy the constraint 'number'.
type T3 = Same<"abc", string>; // error!
// Type 'string' does not satisfy the constraint '"abc"'.

The workaround for TypeScript's rejection of a directly circular constraint is to introduce a third type parameter V. This way, U extends V, and V defaults to T. Essentially, when you use Same<T, U>, it mimics Same<T, U, T>, and TypeScript will throw an error if U extends T is false.

By applying this approach, we can ensure that the keys of AnimalPlaceToAnimalMapType's union align with the ANIMAL_PLACE type, and the array element types in AnimalPlaceToAnimalMapType's union match the ANIMAL_TYPE:

type VerifyMap =
    Same<keyof AnimalPlaceToAnimalMapType, ANIMAL_PLACE> &
    Same<AnimalPlaceToAnimalMapType[keyof AnimalPlaceToAnimalMapType][number], ANIMAL_TYPE>

For a detailed explanation of the utility type implementation and further checks, refer to the original post or the provided Playground link.

Answer №2

type AnimalPlaceToAnimalMapType = {
    [ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish]
    [ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog]
}

Declared as a type that corresponds to its value, without any direct relationship between keys and values.

It would be more explicit to specify that the type equals its corresponding value.

No errors are present as the type is accurate and no assumptions need to be made.

However, if you define a

Record<ANIMAL_PLACE, ANIMAL_TYPE[]>
, all ANIMAL_PLACE entries must be accounted for.

In this example, an error occurs because ANIMAL_PLACE.asia is not declared.

To address this, you can make all keys optional by setting up your const like this:

const AnimalPlaceToAnimalMapRecord: { [K in ANIMAL_PLACE]?: ANIMAL_TYPE[]} = {
    [ANIMAL_PLACE.africa]: [ANIMAL_TYPE.cat, ANIMAL_TYPE.fish],
    [ANIMAL_PLACE.europe]: [ANIMAL_TYPE.dog],
};

While this approach allows for optional keys, additional type-checking will be required due to uncertain key usage.


If you desire a compiler error when omitting an ANIMAL_TYPE, you can structure it as follows:

type AnimalTypeToAnimalPlaceMap = Record<ANIMAL_TYPE, ANIMAL_PLACE[]>;

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

Alter the value by clicking a button within the DynamicRadioGroupModel in ng Dynamic Forms

I am working with ng-dynamic-form (version 6.0.4) and NG Bootstrap in Angular 6. I have a simple question. When a button click event is triggered, I want to change the value in DynamicRadioGroupModel by using the "setValue()" method. However, I am facing ...

Eliminate the eslint@typescript-eslint/no-unused-vars error in TypeScript when the index parameter is not utilized within a map function

Here's the code snippet in question: const reducer = (element:number, index: number) => [element]; //eslint-message. const positionsArray = $.map(this.positions, reducer); I am converting a Float32Array (this.positions) to a JavaScript array. The ...

TS2322 error: What does it mean when the type is both not assignable and assignable?

I've been delving into the world of generics with Java and C#, but TypeScript is throwing me for a loop. Can someone shed some light on this confusion? constructor FooAdapter(): FooAdapter Type 'FooAdapter' is not assignable to type 'A ...

What is the best way to import a reusable component from the theme folder in a React Native project?

I'm interested in importing a Button component that can be reused from the theme folder. The path to the Button component is as follows: \app\theme\components\Button.ts Here is the code for Button.ts: import { typography } from ...

The p-calendar feature is experiencing compatibility issues with Internet Explorer, Edge, and Firefox

While I've had success using primeng p-calendar on Google Chrome, I've encountered an issue where the date-picker does not open upon clicking the text box on other browsers. Below is the snippet of HTML code I utilized: <p-calendar [(ngModel ...

Resolver problem involving Angular HttpClient

Encountering an issue when using Angular 2 with AOT compilation enabled (Angular universal) in a custom resolver. The error message received is as follows: Uncaught (in promise): Error Error: Uncaught (in promise): Error This problem appears to be oc ...

What is the best way to connect input values with ngFor and ngModel?

I am facing an issue with binding input values to a component in Angular. I have used ngFor on multiple inputs, but the input fields are not showing up, so I am unable to push the data to subQuestionsAnswertext. Here is the code snippet from app.component ...

The process of HTML compilation is halted due to the unexpected presence of the forbidden 'null' data type, despite the fact that null cannot actually be a valid value in

Encountering an issue with my HTML code, where the compiler stops at: Type 'CustomItem[] | null | undefined' is not compatible with type 'CustomItem[] | undefined'. Type 'null' cannot be assigned to type 'CustomItem[] ...

Unable to replicate the function

When attempting to replicate a function, I encountered a RootNavigator error Error in duplicate function implementation.ts(2393) I tried adding an export at the top but it didn't resolve the issue export {} Link to React Navigation options documen ...

Tips for transforming TypeScript Enum properties into their corresponding values and vice versa

Situation Imagine having an enum with string values like this: enum Fruit { Apple = "apple", Orange = "orange", Banana = "banana", Pear = "pear" } Users always input a specific string value ("apple", "banana", "orange", "pear") from a drop-down li ...

What is the process for creating a TypeScript type that is generic and includes a keyof property?

Looking to create a generic type that can be used as an argument in a function, but struggling with defining strongly typed property names (specificProperties in the example code snippet). type Config<T> = { specificProperties: keyof T[], dat ...

Using `this` within an object declaration

I am encountering an issue with the following code snippet: const myObj = { reply(text: string, options?: Bot.SendMessageOptions) { return bot.sendMessage(msg.chat.id, text, { reply_to_message_id: msg.message_id, ...options }) ...

typescript: How to restrict an array's type in a specific order

Is there a way to restrict the types of elements in an array in TypeScript without specifying paradigms? For example, instead of defining arrays as follows: const arr:Array<any> = [] I would like to be able to specify a specific order for the arr ...

Manipulating and inserting objects into an array using React and Typescript with an undefined type

As I embark on my TypeScript journey in React, I decided to test my knowledge by creating a simple Todo App. Everything seems to be working fine except for one issue! After adding a new task and hovering over it, I received the following error message (tr ...

Destructuring objects with default values from two related interfaces

In my project, I have defined two interfaces called User and BankUser. The structure of the interface for BankUser looks like this: interface BankUser extends User { banks: { [bank_id: string]: string}; isSuper: boolean; } I am working on a function ...

The default behavior of Angular-Keycloak does not include automatically attaching the bearer token to my http requests

I'm currently working on integrating keycloak-angular into my project, but I'm facing an issue with setting the bearer token as the default for my HTTP requests. "keycloak-angular": "9.1.0" "keycloak-js": "16.0 ...

Is there a way to locate a model using a value within a OneToMany connection?

I am trying to develop a function to retrieve a user model based on a specific value within a OneToMany relationship. Below is the function in question: async getUserViaPlatform(provider: string, id: string) { return await this.webUserRepository. ...

Having trouble with implementing custom checkboxes in a d3 legend?

My attempt at creating checkboxes in d3 has hit a snag. Upon mouse click, the intention is for them to be filled with an "x". Strangely, using d3.select() inside the click-function doesn't seem to work as expected, although adding the letter U for the ...

Using Moment JS to display the days of the upcoming week

I'm in the process of developing a weather application and I need to create code that will display the upcoming week's weather forecast. The only information I have from the server is a "time" entity with a "value" set for next Monday such as "20 ...

I encountered an error when attempting to utilize a recursive type alias in a generic context

When attempting to use a recursive type alias in a generic function, TypeScript v3.7.5 throws the error message: Type instantiation is excessively deep and possibly infinite.(2589). type State = { head: { title: string; description: s ...