Is there a way to assign values of object properties to the corresponding object in TypeScript?

I'm looking for a solution in TypeScript where I can map values of object keys to the same object, and have IntelliSense work correctly. Here's an example that illustrates what I need:

const obj = getByName([
  { __name: 'foo', baz: 'foobar' },
  { __name: 'bar', qux: 'quux' },
]);

obj.foo.__name // should be recognized
obj.foo.baz // should work
obj.foo.quuz // should not be recognized

obj.bar.__name // should be recognized
obj.bar.qux // should work
obj.bar.quuz // should not be recognized

Answer №1

If you're looking for a solution, consider the following code snippet:

export function getByType<
  TType extends string,
  TObject extends {__type: TType},
>(arr: TObject[]): Record<TType, TObject> {
  return arr.reduce((acc, obj) => {
    return {
      ...acc,
      [obj.__type]: obj,
    };
  }, {} as Partial<Record<TType, TObject>>) as Record<TType, TObject>;
}

typescript playground

After testing this solution, I found two issues that need to be addressed:

  1. The resulting object allows keys of any string, potentially causing errors.
  2. The function considers an object with properties like obj.property.subproperty to be valid, even if it doesn't match the specified structure.

To fix the first issue, you can declare the array as readonly:

function getByType<TObject extends {__type: string}>(arr: readonly TObject[]) {
  return arr.reduce((acc, obj) => {
    return {
      ...acc,
      [obj.__type]: obj,
    };
  }, {}) as Record<TObject['__type'], TObject>;
}

const result = getByType([
  { __type: 'example', property1: 'value1' },
  { __type: 'sample', property2: 'value2' },
] as const);

result.nonExistingProperty.unknown // error thrown now!

typescript playground

As for the second issue regarding generics, implementing some type guards may help resolve it if necessary.

Answer №2

The challenge arises when trying to uphold the connection between property names and their corresponding interfaces. It is desired for property baz to exist under key foo, but not under key bar.

I've managed to make it partially functional, but only if you specify __name: 'foo' as const to indicate that the type of this name should be exactly the string 'foo'. Otherwise, TypeScript interprets each name's type as string, leading to a loss in specificity regarding the association between specific names and properties.

// utility function to extract element types from an array
type Unpack<T> = T extends (infer U)[] ? U : never;

type KeyedByName<U extends {__name: string}[]> = {
    [K in Unpack<U>['__name']]: Extract<Unpack<U>, {__name: K}>
}

In the KeyedByName definition, we establish that the value for a key can only be the elements from the array whose __name property matches the type of that key. However, if the key type is just string, this constraint will not be enforced.

When using the notation 'foo' as const, the resulting type of KeyedByName becomes highly specific.

const inputsConst = [
  { __name: 'foo' as const, baz: 'foobar' },
  { __name: 'bar' as const, qux: 'quux' },
];

type K1 = KeyedByName<typeof inputsConst>

This results in:

type K1 = {
    foo: {
        __name: "foo";
        baz: string;
        qux?: undefined;
    };
    bar: {
        __name: "bar";
        qux: string;
        baz?: undefined;
    };
}

We are now able to determine which properties are required and which ones do not exist (can only be undefined).

const checkK1 = ( obj: K1 ) => {

    const fooName: string = obj.foo.__name // valid
    const fooBaz: string = obj.foo.baz // must be a string
    const fooQux: undefined = obj.foo.qux // accessible, but always undefined since it does not exist
    const fooQuuz = obj.foo.quuz // error

    const barName: string = obj.bar.__name // valid
    const barQux: string = obj.bar.qux // must be a string
    const barBaz: undefined = obj.bar.baz // accessible, but always undefined since it does not exist
    const barQuuz = obj.bar.quuz // error
}

However, without utilizing foo as const, this type is no more specific than the generic Record mentioned in @gurisko's response because TypeScript sees both 'foo' and 'bar' as having type string, thereby considering them equivalent.

const inputsPlain = [
    { __name: 'foo', baz: 'foobar' },
    { __name: 'bar', qux: 'quux' },
];

type K2 = KeyedByName<typeof inputsPlain>

Which leads to:

type K2 = {
    [x: string]: {
        __name: string;
        baz: string;
        qux?: undefined;
    } | {
        __name: string;
        qux: string;
        baz?: undefined;
    };
}

In this case, all properties are considered optional regardless of whether they belong to foo or bar.

const checkK2 = ( obj: K2 ) => {

    const fooName: string = obj.foo.__name // valid
    const fooBaz: string | undefined = obj.foo.baz // valid, but could also be undefined
    const fooQux: string | undefined = obj.foo.qux // valid, but could also be undefined
    const fooQuuz = obj.foo.quuz // error

    const barName: string = obj.bar.__name // valid
    const barQux: string | undefined = obj.bar.qux // valid, but could also be undefined
    const barBaz: string | undefined = obj.bar.baz // valid, but could also be undefined
    const barQuuz = obj.bar.quuz // error
}

Playground Link

Answer №3

The content of this response has been inspired by @Linda Paiste's post.

Instead of utilizing plain objects, I have chosen to employ classes as alternatives. To quote Linda:

In the context of KeyedByName, it is established that the value for a key must align with the elements of the array whose __name property corresponds to the type associated with that key. However, if the key type is solely string, no narrowing occurs.

Through the utilization of classes, TypeScript appears to undertake a more efficient method in deducing types during unpacking by scrutinizing the objects using the __types properties mapped to the respective classes themselves. Instead of amalgamating the plain objects together, it accurately infers the correct type, hence causing KeyedByName to yield the following outcome:

class Foo {
  get __name(): 'foo' { return 'foo'; }

  constructor(public baz: string) {}
}

class Bar {
  public readonly __name: 'bar' = 'bar';

  constructor(public qux: string) {}
}

type ObjectsByName = KeyedByName<typeof inputs>;

With reference to ObjectsByName, the resulting evaluation translates to:

type ObjectsByName = {
    foo: Foo;
    bar: Bar;
}

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

Is it necessary to ensure application readiness before proceeding with unit testing exports?

I've been facing a challenge while trying to utilize Jest for unit testing an express API that I've developed. The issue arises when the database needs to be ready before running the test, which doesn't seem to happen seamlessly. In my serve ...

Oops! There seems to be a hiccup: Unable to locate the control with the specified path: 'emails -> 0 -> email'

I am attempting to design a form incorporating a structure like this: formGroup formControl formControl formArray formGroup formControl formControl However, upon clicking the button to add reactive fields and submitting the form ...

Implement the click event binding using classes in Angular 2

If I have the template below, how can I use TypeScript to bind a click event by class? My goal is to retrieve attributes of the clicked element. <ul> <li id="1" class="selectModal">First</li> <li id="2" class="selectModal">Seco ...

Is it possible to utilize an InterleavedBufferAttribute for index values?

I am troubleshooting a code snippet that is throwing an error: const geometry = new THREE.BufferGeometry(); const indices = new THREE.InterleavedBufferAttribute(...); geometry.setIndex(indices); // this is invalid After running this code, I receive a com ...

Challenges encountered while compiling Node.js code with ts-node (Error: Cannot use import statement outside a module)

Trying to compile TypeScript code with NodeJS using this command: npx ts-node src/server.ts An error is thrown: SyntaxError: Cannot use import statement outside a module Following the error's instructions: Warning: To load an ES module, set " ...

In Angular 5 HTTP GET request, the value "null" is being converted to ""null""

I'm currently in the process of transitioning our application from Angular 4 to Angular 5. In Angular 5, when passing an object model as parameters, if one of the values is null, it gets converted to a "null" string which is causing issues for us. Her ...

ESLint not functioning properly on TypeScript (.ts and .tsx) files within Visual Studio Code

After installing the ESLint extension in VSC, I encountered an issue where it was no longer working on the fly for my React project when I introduced Typescript. In the root of my project, I have a .eslintrc file with the following configuration: { "pa ...

Injecting a component in Angular 2 using an HTML selector

When I tried to access a component created using a selector within some HTML, I misunderstood the hierarchical provider creation process. I thought providers would look for an existing instance and provide that when injected into another component. In my ...

Sometimes encounter undefined values after assigning them through the service

One of the challenges I am facing is handling public fields in my service: @Injectable() export class ShareDataService { // Some code public templateForCurrencyCOS; public templateForPercentCOS; public templateForCurrencyCOGS; public te ...

Best Practices for TypeScript and React: How to Handle Component State for Mounted Components

One technique to avoid calling .setState() on an unmounted component is by using a private property like _isMounted to keep track of it, as discussed in a blog post. I have implemented this method as follows: class Hello extends React.PureComponent{ _isM ...

The TypeScript compiler is searching in an external directory for the node_modules folder

My angular 2 project is located in the directory /c/users/batcave/first_project. In that same directory, I have various files such as index.html, systemjs.config.js etc., and a node_modules folder that only contains @types and typescript. This means my @a ...

Guide to Reverting the Two-Way ngModel Binding Data in Angular 2

I am utilizing a form in angular 2 that includes two-way binding data value ([(ngModel)]) to enable both edit and add functionality. When a user selects the edit option on the listing page and modifies the input, the new values automatically appear on the ...

Encountering issues while attempting to transmit several files to backend in React/NestJS resulting in a BAD REQUEST error

My goal is to allow users to upload both their CV and image at the same time as a feature. However, every time I attempt to send both files simultaneously to the backend, I encounter a Bad Request error 400. I have made various attempts to troubleshoot th ...

For editing values that have been dynamically inserted

In my JSON data, there is a variable named address that contains multiple objects (i.e., multiple addresses). I am displaying these multiple addresses as shown in the following image: https://i.sstatic.net/1AG34.png When clicking on a specific address ( ...

Is there a method available that functions akin to document.getelementbyid() in this specific scenario?

Currently, I am tackling a project that involves implementing a search function. My initial step is to ensure that all input is converted to lowercase in order to simplify SQL calls. However, I have encountered a challenge that is proving difficult for me ...

I am having trouble reaching the _groups attribute in angular/d3js

I am encountering an issue when trying to access the "_groups" property in my code: function getMouseDate(scale){ var groupElement = d3.select("#group")._groups[0][0] var xCoordinate = scale.invert(d3.mouse(groupElement)[0]); co ...

How can we implement type guarding for a generic class in TypeScript?

Implementing a generic class in TypeScript that can return different types based on its constructor parameter. type Type = 'foo' | 'bar'; interface Res { 'foo': {foo: number}; 'bar': {bar: string}; } class ...

Mozilla struggles to interpret JSON data

When using Angular2 (RC4), I utilize this code snippet to retrieve data from my WebApi: getAppointment(id: number): Observable<Event> { return this._http.get(this._serviceUrl + 'get/' + id) .map(this.extractData) .catch ...

When using TypeORM's save() method with an array of objects, the @PrimaryColumn() annotations are ignored, resulting

My current situation involves an entity called Point: @Entity() export class Point { @PrimaryGeneratedColumn('uuid') id: string; @IsUUID() @PrimaryColumn({ type: 'uuid', ...

Upgrade Angular from 12 to the latest version 13

I recently attempted to upgrade my Angular project from version 12 to 13 Following the recommendations provided in this link, which outlines the official Angular update process, I made sure to make all the necessary changes. List of dependencies for my p ...