Converting an array of objects into a unified object and restructuring data types in TypeScript

A few days back, I posted a question regarding the transformation of an array of objects into a single object while preserving their types. Unfortunately, the simplified scenario I provided did not resolve my issue.

In my situation, there are two classes: Int and Bool, both of which are subclasses of the Arg class. Additionally, there exist two factory functions called createInt and createBool.

class Arg<Name extends string> {

    protected name: Name

    public constructor(name: Name) {
        this.name = name
    }

}

class Int<Name extends string> extends Arg<Name> {}
class Bool<Name extends string> extends Arg<Name> {}

const createInt = <Name extends string>(name: Name) => new Int<Name>(name)
const createBool = <Name extends string>(name: Name) => new Bool<Name>(name)

Now, I aim to define arguments (Int or Bool with specific names) in an array and create a function that accepts mapped types of these arguments (Number for Int, Boolean for Bool) as input.

const options = {
    args: [
        createInt('age'),
        createBool('isStudent')
    ],
    execute: args => {
        args.age.length // This should trigger an error stating that property 'length' does not exist on type 'number'.
        args.name // This should produce an error since property 'name' doesn't exist in 'args'.
        args.isStudent // This should work fine as it is a boolean.
    }
}

I came across a question related to mapping types, but I'm unsure how to map the array of `args` to an object of `args`, thereby losing information about argument types. At the moment, I only have strings there without maintaining details about argument types.

type Options = {
    args: Arg<string>[],
    execute: (args: Record<string, any>) => void
}

Is there a way to achieve this and retain the argument types within the execute function? You can access a demo here.

Answer №1

Your provided code example has a few issues that need addressing before we can achieve a working solution. Let's tackle each problem step by step to reach a functional piece of code.


Firstly, the definition of the Arg<N> class only defines the name strongly, which corresponds to a property key. If you want both the compiler and your code to infer information about the type of the value associated with the property, you will need to enhance the Arg definition accordingly. For instance:

class Arg<N extends string, V> {
  public constructor(protected name: N, protected value: V) { }
}

In this updated version, we introduced a second generic type parameter V to represent the value's type. This modification ensures that the compiler understands the relationship between the property name and its corresponding value type.

To reinforce this understanding for integers and booleans, we can extend Int and Bool:

class Int<N extends string> extends Arg<N, number> { }
class Bool<N extends string> extends Arg<N, boolean> { }

Now, Int<N> is recognized as housing a number property, and Bool<N> is identified as holding a boolean attribute. This understanding is crucial for the proper functioning of the subsequent code.

Next, let's address the absence of a specific TypeScript type that precisely matches your Options structure:

type TooWideOptions = {
  args: readonly Arg<string, any>[],
  execute: (args: never) => void
}

The issue here is that while

TooWideOptions</code captures valid values for <code>args
, it also accepts invalid ones due to not restricting the callback parameter of the execute method correctly. To properly account for this constraint, we need to make Options generic:

type Options<A extends readonly Arg<any, any>[]> = {
  args: readonly [...A],
  execute: (args: ArgArrayToObject<A>) => void
}

With Options<A>, where A denotes an array of Arg elements, and the callback parameter for execute is typed as ArgArrayToObject<A>, we establish the necessary constraints.


To realize this implementation, consider the following approach:

type ArgArrayToObject<A extends readonly Arg<any, any>[]> = 
  { [T in A[number]
     as T extends Arg<infer N, any> ? N : never
  ]: T extends Arg<any, infer V> ? V : never } extends 
  infer O ? { [K in keyof O]: O[K] } : never;

This type iterates over the elements within A to construct a mapped type where the keys are remapped based on assigned types. Each element represents an Arg<N, V>, using N for key names and V for value types.

By utilizing a generic function like asOptions, you enable the compiler to deduce the appropriate A for you:

const asOptions = <A extends readonly Arg<any, any>[]>(options: Options<A>) => options;

Although this function simply returns its input without runtime implications, it aids in constraining options during compilation to match the type

Options<A></code where the compiler infers <code>A
.

Finally, leveraging these adjustments results in operational code that enforces the linkage between the args property and the execute method defined within an Options-like type.

Access Playground link for further exploration

Answer №2

Here is the solution you've been seeking

class Arg<Name extends string> {

    name: Name

    public constructor(name: Name) {
        this.name = name
    }

}

class Int<Name extends string> extends Arg<Name> {}
class Bool<Name extends string> extends Arg<Name> {}

const createInt = <Name extends string>(name: Name) => new Int<Name>(name)
const createBool = <Name extends string>(name: Name) => new Bool<Name>(name)

type Unpacked<T> = T extends (infer U)[] ? U : T;

type Options <T extends (Int<string> | Bool<string>)[], V extends Unpacked<T> = Unpacked<T>>= {
    args: T,
    execute: (args: Record<V['name'], V>) => void
}

const args = [
      createInt('age'),
      createBool('isStudent')
  ]

const options: Options<typeof args> = {
    args,
    execute: args => {
        args.age
        args.age.length // Error, 'length' does not exist on type 'number'.
        args.name // Error, property 'name' does not exist in 'args'.
        args.isStudent // Ok, boolean.
        
    }
}

playground

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

Creating a new function within the moment.js namespace in Typescript

I am attempting to enhance the functionality of the moment.js library by adding a new function that requires a moment() call within its body. Unfortunately, I am struggling to achieve this. Using the latest version of Typescript and moment.js, I have sear ...

What are the steps for integrating and expanding a JavaScript library using rollup, TypeScript, and Angular 2?

I am currently working on a project called angular2-google-maps-test and I am interested in integrating and expanding upon the JS library found at js-marker-clusterer npm install --save js-marker-clusterer It seems that this library is not structured as ...

Adding child arrays to a parent array in Angular 8 using push method

Upon filtering the data, the response obtained inside the findChildrens function is as follows: My expectation now is that if the object length of this.newRegion is greater than 1, then merge the children of the second object into the parent object's ...

Best practice for managing asynchronous calls and returns in TypeScript

I’ve recently started working on an Ionic 4 project, delving into the realms of Javascript / Typescript for the first time. Understanding async / await / promise seems to be a bit challenging for me. Here’s the scenario: In one of the pages of my app ...

Retrieving the row value of a checkbox in an Angular table

I'm facing a challenge where I have a table with three columns, one of which is a checkbox. Here is an image for reference: https://i.sstatic.net/4U6vP.png Here is the code snippet: <div nz-row> <nz-table nz-col nzSpan="22" [nzLoading] ...

Hear and register keypress in Angular service

I offer a dialog service Check it out below @Injectable() export class HomeDialogService { public constructor(private readonly dialogService: DialogService, private readonly userAgent: UserAgentService) {} @HostListener('document:keydown.escape ...

Ways to cancel a subscription once a specific parameter or value is met following a service/store interaction

I am working with a service that provides a changing object over time. I need to unsubscribe from this service once the object contains a specific property or later when the property reaches a certain value. In situations like these, I typically rely on t ...

What steps should I take to set up an automated polling system for real-time data updates in Angular?

Hello everyone, I am currently delving into the world of Angular and facing a challenge with refreshing service data automatically by making API requests at regular intervals. The focus is on a particular service where I aim to update the shopPreferences f ...

Form validation errors were detected

Currently, I am working with a formgroup that contains input fields with validations set up in the following manner: <mat-form-field class="mat-width-98" appearance="outline"> <mat-label>Profession Oc ...

Automate the compilation of Typescript project references by creating a solution that allows for passing unique options to each

When compiling or building a project with references programmatically, I make use of the ts.createSolutionBuilder API. The challenge I face is that in my specific scenario, I do not have individual tsconfig.json files stored on the filesystem for each pac ...

What is the rationale behind ngOnInit not being a private method in Angular?

After extensively checking both code samples and even the official documentation, I am still unable to find the information I need. Turning to Google has also yielded no results. The question that baffles me is: ngOnInit() { ... } Why do we use this syn ...

"Troubleshooting: Unspecified getInitialProps in Nextjs when passing it to a layout component

Greetings, I am a newcomer to Next.js and facing an issue with passing dynamic properties to the header component. Despite using getInitialProps in Next.js successfully, I encounter the problem of receiving 'UNDEFINED' when these properties are p ...

Troubleshooting TestBed: Resolving the StatusBar Provider Error

After reading an informative article on testing Ionic2 projects with TestBed, I encountered difficulties when trying to replicate the example in my own environment. When attempting to initiate tests at Step 3, I encountered the error message stating "No pr ...

Angular 6 ActivatedRoute Parameters

I'm having trouble retrieving the data of each record using ActivatedRoute. I've been able to get the ID for each one, but I can't seem to access the other data. Any suggestions? Check out my stackblitz example: https://stackblitz.com/edit/ ...

The deno bundle operation failed due to the absence of the 'getIterator' property on the type 'ReadableStream<R>'

When attempting to run deno with bundle, an error is encountered: error: TS2339 [ERROR]: Property 'getIterator' does not exist on type 'ReadableStream<R>'. return res.readable.getIterator(); ~~~~~~~~~~~ ...

Finding the total of values within an array in Angular 2 and Ionic 2 by utilizing *ngfor

As I work on developing a mobile application using Ionic 2, my current task involves calculating the total allocation sum (Course.allocation) for each year per horse in the database. For instance: Table: Course (Race): [Id_course: 1, allocation: 200, dat ...

Customize the color of the Material-UI Rating Component according to its value

Objective I want to dynamically change the color of individual star icons in a Ratings component (from material-ui) based on the value selected or hovered over. For example, hovering over the 1st star would turn it red, and hovering over the 5th star woul ...

Incapable of acquiring the classification of the attribute belonging to the

Is it possible to retrieve the type of an object property if that object is stored in a table? const records = [{ prop1: 123, prop2: "fgdgfdg", }, { prop1: 6563, prop2: "dfhvcfgj", }] const getPropertyValues = <ROW extends o ...

Inoperative due to disability

Issue with Disabling Inputs: [disabled] = true [disabled] = "isDisabled" -----ts > ( isDisabled=true) Standard HTML disabler disable also not functioning properly [attr.disabled] = true [attr.disabled] = "isDisabled" -----ts > ( isDisabled=true) ...

How to format dates with month names in various languages using the date pipe

In my HTML code, I have set up the date display like this: <span >{{ item.lastModified | date : 'MMM d, y' }}</span> As a result, the displayed date looks something like Jul 20, 2021. However, when I switch my browser's language ...