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

Enhancing Angular functionality with the addition of values to an array in a separate component

I need help with adding a value to an array in the 2nd component when a button in the 1st component is clicked. I am working on Angular 4. How can I achieve this? @Component({ selector: 'app-sibling', template: ` {{message}} <butt ...

Displaying an error message following the dynamic retrieval of the input field's value

How can I display an error message when a specific field with a value of 0 is not filled out in my Angular reactive forms? In my form, I have added a dropdown which is mandatory and I have implemented validators to ensure it is required. The validator work ...

Error: The version of @ionic-native/[email protected] is not compatible with its sibling packages' peerDependencies

When attempting ionic cordova build android --prod, the following error occurred: I have tried this multiple times. rm -rf node_modules/ rm -rf platforms/ rm -rf plugins/ I deleted package.lock.json and ran npm i, but no luck so far. Any ideas? Er ...

TypeScript requires that the `includes` function must have the same type parameter for both input and

When working with TypeScript, I've encountered an interesting dilemma regarding the use of the Array.Prototype.includes function. It seems that this function requires me to pass in the same type as in the original array, but isn't the purpose of ...

Examining Axios HttpService piping through a NestJS middleware in a unit test

A middleware function retrieves a JSON document from a microservice endpoint and appends it to the request. The good path test is successful, but I'm struggling to make the bad path test throw a ForbiddenException and stop it from invoking next(). W ...

Enhancing interface properties with type safety in a function declaration using Typescript

Consider the following scenario: interface Steps { stepOne?: boolean; stepTwo?: boolean; stepThree?: boolean; } let steps: Steps = {}; function markStepDone (step: ???) { steps[step] = true; } markStepDone('anything'); Is there a wa ...

How to ensure Angular mat-button-toggle elements are perfectly aligned within their respective groups

https://i.stack.imgur.com/Wjtn5.png Hello there, I'm trying to make the numbers in the first group match the style of the second group (noche, mañana...). I've set both the group and individual element width to 100%, but the numbers beyond 22 ...

After utilizing the d3-scale function to declare an object, my developer visual suddenly ceases to function

Upon completing a section of a Power BI tutorial, the developer encountered a visual that displayed nothing but a blank page (despite running correctly). Unable to pinpoint the issue, debugging was initiated. The following test code snippet for debugging w ...

Is it possible to achieve pagination by simply dragging the scroll bar to the top or bottom position?

Recently, I've been working on implementing a pagination function for a list of items. The pagination currently works well with scrolling events - it automatically moves to the next page when scrolling to the bottom, and to the previous page when scro ...

Error: While working in an Angular project, a TypeError occurs because the property '****' cannot be read when used within a forEach loop

As I attempt to iterate over this.data.members and perform certain actions within the forEach loop on this.addedUsers, I encounter a TypeError: Cannot read property 'addedUsers' of undefined. Interestingly, I can access this.data.members outside ...

I am currently facing an issue related to the length property. It is showing an ERROR TypeError: Cannot read property 'length' of undefined

Is it recommended to set the length to be inherited from Angular right? If so, why am I getting this error: "MyPostsComponent.html: 7 ERROR TypeError: Cannot read the 'length' of undefined property" when fileList.length is greater than 0? onFile ...

Instructions for adding a select dropdown feature in Angular 6 that includes a search filter. Additionally, tips on how to filter objects by their name property

I need to add an auto-complete feature in my Angular 6 app where the data is displayed as objects in a dropdown and filtered as we type. **template.html** <mat-form-field > <input matInput [matAutocomplete]="auto" [formControl]="customerFi ...

How do I implement data range filtering in Typescript?

Seeking assistance with filtering data by date range and forwarding the results to the client. The objective is to extract tickets created within specific dates, but I keep encountering a console error which is proving challenging to resolve. var befor ...

Angular Universal involves making two HTTP calls

When using Angular Universal, I noticed that Http calls are being made twice on the initial load. I attempted to use transferState and implemented a caching mechanism in my project, but unfortunately, it did not resolve the issue. if (isPlatf ...

Testing the receiveMessage function in SQS using Jest unit tests

Struggling to find the right approach for unit testing this function. I almost have it, but can't quite nail it down. Take a look at the function below: receiveMessage(callback: Function): any { this.sqs.receiveMessage( this.params, ...

Applying Validators manually in Angular 2 beta 17

We are currently working on a legacy project that needs to be maintained until the final version with angular-final is deployed. Once we upgrade to the final version, I will be able to easily apply conditional Validators using: this.myForm.controls[&apos ...

I am facing issues with my filtering functionality on the Angular Typescript mat-table component

I am facing issues while trying to filter my table, the reaction is not correct and I can't seem to find where I went wrong. Here is the HTML part : <mat-form-field appearance="outline"> <mat-label>Search</mat-label> & ...

Error message in Angular when promises are not defined

Recently, I started working with promises for the first time. I have a function that returns a promise: public DatesGenerator(futercampaign: ICampaign, searchparam: any, i: number): ng.IPromise<any> { return this.$q((resolve, reject) => { ...

Should the input field only contain spaces, a validation error will be triggered by the user

I am currently working on an Angular form implementation that allows users to enter their phone numbers. I have integrated a custom directive called appPhoneExtMask for formatting the phone numbers and have also implemented Angular form validation for both ...

Accessing JSON data stored locally and initializing it into a TypeScript variable within a React application

I'm new to working with JSON arrays and I'm facing a challenge. I am looking for a way to load data from a JSON file into a Typescript variable so that I can perform a specific operation that involves arrays. However, I'm unsure of how to ac ...