Is there a way to ensure that the value of an object matches its corresponding key through type enforcement

I have objects that implement types as shown below:

type TMyObject<T extends string> {
  category: T
}

I am trying to store them statically in another object while ensuring that the key of this second object matches the value of the category field, like this:

const myObject: TMyObject<'foo'> = { category: 'foo' }

const dictionary = {
  foo: myObject, // correct
  bar: myObject, // not valid: key 'bar' does not match myObject's category
}

This issue arises because I have interfaces that extend IMyObject and set the category field to a specific value, as demonstrated:

type TMyFooObject = IMyObject<'foo'>

I have spent a significant amount of time attempting to create a type for the dictionary object that functions as described, but I am struggling to find a solution ^^

It is important to note that the category field and the potential types extending TMyObject are dynamic, so a simple union cannot be used here...

Thank you for taking the time to read and potentially respond to this inquiry!

Answer №1

Using mapped types, we can assign values to an object based on their keys.

type Dictionary<Keys extends string> = {
    [K in Keys]: MyCustomObject<K>
}

If we have prior knowledge of all categories, we can create a map with those as the keys.

const dictionary: Dictionary<'foo' | 'bar'> = {
  foo: { category: 'foo' }, // valid
  bar: { category: 'foo' }, // error: Type '"foo"' is not assignable to type '"bar"'
}

If you wish to restrict the categories but allow some to be missing, consider using

Partial<Dictionary<Category>>
.

This approach works well if your types already provide a union type of all possible categories.

The key-value matching validation for objects can be achieved by creating objects through an identity function. A generic function helps infer the Keys type from the object.

const createDictionary = <Keys extends string>(dict: Dictionary<Keys>) => dict;

const newDict = createDictionary({
  foo: { category: 'foo' }, // valid
  bar: { category: 'foo' }, // error: Type '"foo"' is not assignable to type '"bar"'
});
// newDict now has type: Dictionary<"foo" | "bar">

Check out Typescript Playground Link

Answer №2

Whenever IMyObject includes a category of type string, it becomes a runtime check rather than a compile-time check.

To make sure the check is done at compile time, you can define specific types for each case:

interface IFooObject {
  category: 'foo';
  foo: number;
  //other foo props
}

interface IBarObject {
  category: 'bar';
  bar: number;
  //other bar props
}

type IObject = IFooObject | IBarObject;

const myObject: IObject = { category: 'foo', foo: 123 }

type Disco<T> = T extends { category: infer U } ? U extends string ? { [P in U]: T } : never : never;

const disco: Disco<IObject> = {
  foo: myObject, // valid
  bar: myObject, // now this fails!
};

Playground link

You can also utilize the generic type, but in that scenario, you need to pass it through like so:

type TMyObject<T extends string> = {
  category: T
}

const myObject: TMyObject<'foo'> = { category: 'foo' }

type Disco<T> = T extends { category: infer U } ? U extends string ? { [P in U]: T } : never : never;

const dico: Disco<typeof myObject> = {
  foo: myObject, // valid
  bar: myObject, // invalid: 'bar' key does not match myObject.category
}

Playground link

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

Exploring and adding elements in a sophisticated array or object through recursive searching

After referring to this plunker https://plnkr.co/edit/CIGAA5BmiKU4hCMsOaIB?p=preview, I now require dynamic array operations. [ { title: 'Menu 1', id :1, hide : true, children: [], }, { title: 'Menu 2', hide : t ...

Utilizing TypeScript Generics to Dynamically Set Tag Names in React

I am working on a straightforward polymorphic React component that is designed to render only tag names (such as span) and not custom React components (like MyComponent). I believe this can be achieved using JSX.IntrinsicElements. Here is the code snippet ...

Reducing image file sizes in Ionic 3

I have been struggling to compress an image client-side using Ionic 3 for the past couple of days. I have experimented with: ng2-img-max - encountered an error when utilizing the blue-imp-canvas-to-blob canvas.toBlob() method (which is a dependency of ng2 ...

What is the reason for TypeScript's prioritization of arguments over assignment in generic inference?

When it comes to Typescript generics inference, the priority is given to arguments over assignment. This means that any param props are automatically converted into type unknown, even if they are assigned to a variable whose type param is set to an object ...

Utilizing TypeScript function components within styled-components

I have developed a versatile component for displaying a DataGrid like this: interface Props<T extends unknown> { header: HeaderItem[] data?: T[], className?: string, children?: (row: T) => React.ReactElement[] } const DataGrid = & ...

What is the reason for the index type being defined twice?

Here is an example from the official TypeScript documentation: class Animal { name: string; } class Dog extends Animal { breed: string; } // Error: indexing with a 'string' will sometimes get you a Dog! interface NotOkay { [x: numbe ...

Issue with Readonly modifier not functioning as expected in Angular/Typescript

My goal is to create a component property that is read-only. However, I am facing an issue where the readonly modifier does not seem to have any effect. View example on stackblitz According to the documentation, once I initialize the cars property in the ...

Error: The function being called in <class> is not recognized as a function

I have a unique situation with my component setup export class Component1Component implements OnInit { public greetings: string =""; constructor(private greeter: Greeter) { } ngOnInit() { this.greetings = this.greeter.sayHello(); } } The structur ...

Error in class constructor due to a Typescript typeguard issue

Within my `BigNumber` class, the constructor is designed to take an initializing argument that can be a string, number, or another `BigNumber`. Depending on the type of the argument provided, the constructor will initialize an instance of `BigNumber` using ...

The challenge of handling Set type in TypeScript errors

I'm currently facing two errors while trying to convert a function to TypeScript. The issue lies with the parameters, which are of type Set import type {Set} from 'typescript' function union<T>(setA: Set<T>, setB: Set<T>) ...

I am inputting an array in which I am certain about the type of the initial value, however, I am uncertain about the length of the array and the types of the remaining values

Within this function, the first argument must be a function while the remaining arguments could be of any type and their length is unknown. How should I define the array type to have a known first element but unknown length and types for the rest? // I a ...

Trouble with Typescript in VSCode made easy

Setting up a VSCode environment for working with TypeScript v2.03 has been challenging. Beginning with a simple vanilla javascript snippet that can be tested in node via the integrated terminal. function Person() { this.name = ""; } Person.prototy ...

Is there a TypeScript definition file available for Google Charts?

I am interested in implementing Google Charts with TypeScript, however I am uncertain about the availability of .d.ts files for Google Charts. Does anyone have information on where these files can be downloaded? If so, could you please provide a link? ...

Unable to invoke the AppComponent function within ngOnInit due to error message: "Object does not have the specified property or method"

I'm a novice in Angular and I am attempting to invoke my function setCenter2() from the AppComponent class within the ngOnInit function of the same class. Is this achievable? Whenever I try to call my function by clicking on the map (using OpenStreetM ...

What is the best way to execute an asynchronous request in AngularJs and Typescript?

Despite its simplicity, I can't seem to find an answer to this question as I am relatively new to Javascript, Typescript, and Angular. I have searched extensively without success. My objective is to send a basic request to a server and handle the res ...

What steps should I follow to properly set up my tsconfig.json in order to ensure that only the essential files are included when executing npm run build

Introduction I am seeking guidance on how to correctly set up my tsconfig.json file to ensure only the necessary files are included when running npm run build for my projects. I want to avoid any unnecessary files being imported. Query What steps should ...

Troubleshooting: Unable to locate .vue.d.ts file during declaration generation with Vue, webpack, and TypeScript

Currently, I am developing a library using Typescript and VueJS with webpack for handling the build process. One of the challenges I'm facing is related to the generation of TypeScript declaration files (.d.ts). In my source code, I have Typescript ...

Issues arise when attempting to enforce type-safety in TypeScript while using the JSON.parse

Is it possible that type-safety is compromised in TypeScript when dealing with JSON parsing? I should be triggering an error, but I'm not: interface Person { name: string } const person: Person = somePossibleFalsey ? JSON.parse(db.person) : undefi ...

Sharing parameters between pages in Angular IonicPassing parameters between pages within an Angular Ionic application

Is there a way to pass parameters from the signup page to the signupotp page successfully? I am facing an issue where the OTP on the signupotp page is not being recognized because the parameters (email and mobile) are not getting passed properly. In my bac ...

Embark on your project with Adonisjs, utilizing both http and https protocols

Currently, I am seeking to have both HTTP and HTTPS operational in my adonisjs project. At the moment, my server.ts file is configured for HTTPS as shown below: import 'reflect-metadata' import sourceMapSupport from 'source-map-support' ...