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

Angular 5 Dilemma: Exporting UI Components without Locating Template

My current project involves developing UI Components that will be used in various web projects within the company. Our plan is to publish these UI components as an npm package on our local repository, and so far, the publishing process has been successful. ...

How Angular can fetch data from a JSON file stored in an S3

I am attempting to retrieve data from a JSON file stored in an S3 bucket with public access. My goal is to parse this data and display it in an HTML table. http.get<Post>('https://jsonfile/file.json').subscribe (res => { cons ...

Storing the subscription value retrieved from an API in a global variable

I am trying to find a way to make the data retrieved from an API accessible as a global variable in Typescript. I know that using subscribe() prevents this, so I'm looking for a workaround. Here is the API code: getResultCount(category:any):Obs ...

Angular fails to include the values of request headers in its requests

Using Django REST framework for the backend, I am attempting to authenticate requests in Angular by including a token in the request headers. However, Angular does not seem to be sending any header values. Despite trying various methods to add headers to ...

Retrieve the :id parameter from the URL as a numerical value in Node.js using Typescript

Is there a way to directly get the :id param from the URL as a number instead of a string in order to easily pass it on to TypeORM for fetching data based on a specific ID? Currently, I am using the following approach where I have to create an additional ...

What is the best way to retrieve every single element stored in an Object?

On a particular page, users can view the detailed information of their loans. I have implemented a decorator that retrieves values using the get() method. Specifically, there is a section for partial repayments which displays individual payment items, as d ...

"Uh-oh! Debug Failure: The statement is incorrect - there was a problem generating the output" encountered while attempting to Import a Custom Declarations File in an Angular

I am struggling with incorporating an old JavaScript file into my Angular service. Despite creating a declaration file named oldstuff.d.ts, I am unable to successfully include the necessary code. The import statement in my Angular service seems to be worki ...

The element is implicitly classified as an 'any' type due to the index expression not being of type 'number'

Encountering a specific error, I am aware of what the code signifies but unsure about the correct interface format: An error is occurring due to an 'any' type being implicitly assigned as the index expression is not of type 'number'. ...

Retrieve the initial token from a union, referred to as an "or list," in Typescript

Is there a way to define a generic type F with the following behavior: type X = F<'a'|'b'|'c'> should result in X being 'a'. And if type X = F<'alpha'|'beta'|'gamma'|'del ...

TypeScript's robustly-typed rest parameters

Is there a way to properly define dynamic strongly typed rest parameters using TypeScript 3.2? Let's consider the following scenario: function execute<T, Params extends ICommandParametersMapping, Command extends keyof Params, Args extends Params[C ...

Material UI React Autocomplete Component

I'm currently working on integrating an Autocomplete component using the Material UI library. However, I've encountered a challenge - I'm unsure of how to properly pass the value and onChange functions, especially since I have a custom Text ...

Error: SvelteKit server-side rendering encountered a TypeError when trying to fetch data. Unfortunately, Express is not providing a clear TypeScript stack trace

I've been monitoring the logs of the SvelteKit SSR server using adapter-node. After customizing the server.js to utilize Express instead of Polka, I noticed some errors occurring, particularly when the fetch() function attempts to retrieve data from ...

Changing the names of the remaining variables while object destructuring in TypeScript

UPDATE: I have created an issue regarding this topic on github: https://github.com/Microsoft/TypeScript/issues/21265 It appears that the syntax { ...other: xother } is not valid in JavaScript or TypeScript, and should not compile. Initial Query: C ...

Implementing TypeScript with styled components using the 'as' prop

I am in the process of developing a design system, and I have created a Button component using React and styled-components. To ensure consistency, I want all my Link components to match the style and receive the same props as the Button. I am leveraging t ...

What is the process for incorporating personalized variables into the Material Ui Theme?

In the process of developing a react app with TypeScript and Material UI, I encountered an issue while attempting to define custom types for my themes. The error message I received is as follows: TS2322: Type '{ mode: "dark"; background: { default: s ...

how to adjust the width of a window in React components

When attempting to adjust a number based on the window width in React, I encountered an issue where the width is only being set according to the first IF statement. Could there be something wrong with my code? Take a look below: const hasWindow = typeof ...

Guide on deactivating the div in angular using ngClass based on a boolean value

displayData = [ { status: 'CLOSED', ack: false }, { status: 'ESCALATED', ack: false }, { status: 'ACK', ack: false }, { status: 'ACK', ack: true }, { status: 'NEW', ack ...

Guide on specifying a type for a default export in a Node.js module

export const exampleFunc: Function = (): boolean => true; In the code snippet above, exampleFunc is of type Function. If I wish to define a default export as shown below, how can I specify it as a Function? export default (): boolean => true; ...

What is the method for defining a function within a TypeScript namespace?

Suppose there is a namespace specified in the file global.d.ts containing a function like this: declare namespace MY_NAMESPACE { function doSomething(): void } What would be the appropriate way to define and describe this function? ...

Currency symbol display option "narrowSymbol" is not compatible with Next.Js 9.4.4 when using Intl.NumberFormat

I am currently utilizing Next.JS version 9.4.4 When attempting to implement the following code: new Intl.NumberFormat('en-GB', { style: 'currency', currency: currency, useGrouping: true, currencyDisplay: 'narrowSymbol'}); I ...