What is the proper way to create a generic class that can produce various output types while implementing a basic interface?

An abstract class is being created here to transform one type of object into another, with a simple interface comprising id:string and type:string.

The intention behind the class definition is to emphasize that it will produce an assembly and during instantiation specify WHICH particular assembly will be generated.

Encountered Compilation Error (at line 79 return assembly;):

Error: Type 'Assembly' cannot be assigned to type 'Out'. 'Assembly' can be assigned to the constraint of type 'Out', but 'Out' may potentially be instantiated with a distinct subtype of the restriction 'Assembly'.(2322)

Typescript Code:

interface IAssemblyRequirements {
    id: string;
    type: string;
}

interface IData extends IAssemblyRequirements {
    attributes: any;
}

interface IFooBarData extends IData {
    attributes: {
        start: string;
    }
}

interface IBazBarData extends IData {
    attributes: {
        chickens: number;
    }
}

const foobarData: IFooBarData = {

    id: "1",
    type: "foobar",
    attributes: {
        start: "Dec 1, 2020"
    }

}

const bazbarData:IBazBarData = {

    id: "2",
    type: "bazbar",
    attributes: {
        chickens: 9
    }

}

class Assembly implements IAssemblyRequirements {

    id:string;
    type:string;

    constructor(data: IData) {
        this.id = data.id;
        this.type = data.type;
    }

}

class FooBar extends Assembly {
    start:Date;
    constructor(data: IFooBarData) {
        super(data);
        this.start = new Date(data.attributes.start);
    }
}

class BazBar extends Assembly {
    chickens: number;
    constructor(data: IBazBarData) {
        super(data);
        this.chickens = data.attributes.chickens;
    }
}

const typeAssemblers:{ [key:string]: typeof Assembly } = {
    foobar: FooBar,
    bazbar: BazBar
}

class Assembler<In extends IData, Out extends Assembly> {

    assemble(input: In): Out {
        const assembly = new typeAssemblers[ input.type ]( input );
        return assembly;
    }

}

const assembler = new Assembler<IFooBarData, FooBar>();
const assembly = assembler.assemble(foobarData);

Answer №1

Using generics, you can illustrate the connection between input and output types, but the compiler may struggle to confirm that your Assembler.assemble() method complies with it. This challenge might be addressed in the future with the introduction of correlated record types, as discussed here. In the meantime, utilizing type assertions might be necessary when working with constructs like

new typeAssemblers[input.type](input)
. So, keep this in mind moving forward.


Your current problem lies in the fact that IFooBarData and IBazBarData have a type property defined as string, which needs to be restricted to specific string literal types like "foobar" and "bazbar" for successful implementation. Otherwise, an unexpected value such as "oopsie" could cause runtime errors. Here are modified definitions incorporating these constraints:

interface IFooBarData extends IData {
    attributes: {
        start: string;
    }
    type: "foobar"
}

interface IBazBarData extends IData {
    attributes: {
        chickens: number;
    }
    type: "bazbar"
}

Furthermore, annotating typeAssemblers with

{ [key:string]: typeof Assembly }
results in loss of important type information. Instead, allowing TypeScript to infer the type or defining a narrower type explicitly is recommended. Here's how it can be done:

const typeAssemblers = {
    foobar: FooBar,
    bazbar: BazBar
}
type TypeAssemblers = typeof typeAssemblers;

The inferred type here is

{foobar: typeof FooBar; bazbar: typeof BazBar;}
.


Lastly, the Assembler class should only have a single generic type parameter. It is crucial to maintain the connection between the In type and the Out type defined by TypeAssemblers. The output type can be computed based on the input type, ensuring consistency. Below is the corrected version of the Assembler class with the previously mentioned type assertions:

class Assembler<I extends IFooBarData | IBazBarData> {
    assemble(input: I) {
        return new typeAssemblers[input.type](
            input as any
        ) as InstanceType<TypeAssemblers[I["type"]]>;
    }
}

With this adjustment, testing the functionality becomes straightforward:

const assembler = new Assembler<IFooBarData>();
const assembly = assembler.assemble(foobarData); // FooBar
console.log(assembly.start.getFullYear()); // 2020

The compiler now correctly identifies assembly as a FooBar.


I hope this guidance sets you on the right path. Good luck!

Access Playground link with code snippet

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 considered appropriate to return null in a didReceiveResponse callback function?

In my implementation, I have a callback called didReceiveResponse within a class that extends RESTDataSource. In this method, I return null when the response status is 404. However, due to the typing definition of RESTDataSource.didReceiveResponse, it seem ...

Attempting to send numerous identifiers in an API request

I encountered a problem while working on a function in Angular that involves pulling data from an API. My goal is to enhance a current segment to accommodate multiple IDs, but I face difficulties when attempting to retrieve more than one ID for the API que ...

One efficient way to iterate through an object and modify its values in a single line of code

_shop: { [key: string]: string[] } = { fruits: ['Apple', 'Orange'], vegetables: ['Tomato', 'Onions'] } Can a one-liner code be used to modify the values of _shop and return it in a specific format? The desired outp ...

Error: Missing npm install -g @angular/cli@latest package in devDependencies section

ng build is causing an issue that The error reads: Unable to Find npm install -g @angular/cli@latest in devDependencies. When I attempt to start the application using npm start, it works fine. However, while trying to build a file, I encounter this er ...

What is the best way to eliminate square brackets from keys within an array of objects in TypeScript?

I am currently working on a task to eliminate all square brackets from the keys in the entries field within an array of objects: data: [ {title: "Title1", entries: { 'Entry1': 333333, '[ABC]Entry2': 1234, 'Entry3' ...

"Dealing with conflicts between RMQ and TypeORM in a NestJS

Every time I try to use TypeOrm, RMQ crashes. I can't figure out why. Utilizing the library golevelup/nestjs-rabbitmq has been a struggle for me. I've spent 7 hours trying to resolve this issue. @Module({ imports: [ ConfigModule.f ...

Potential uncertainty in Angular FormControl email validation due to potential null Object

Whenever I run the command ng s --aot, I encounter this message: Object is possibly 'null'. I've been trying various solutions all morning to resolve it, but without success. The issue seems to be related to objects like email.valid, dirty, ...

Generic type array does not display property

I feel like I must be overlooking something. It seems too straightforward to be causing issues for me. Database.ts export class Database { id: number; } search-input.ts import { Database } from './../resources/database'; import { Inje ...

Tips on determining the data type for a personalized useFetch hook

I want to develop a useFetch hook to handle various types of data like objects and arrays. Is there a way to make it dynamic without specifying a specific type? Sample function useRequest(url: string, method: Method, data: any) { const [response, s ...

Using TypeScript to consolidate numerous interfaces into a single interface

I am seeking to streamline multiple interfaces into one cohesive interface called Member: interface Person { name?: { firstName?: string; lastName?: string; }; age: number; birthdate?: Date; } interface User { username: string; emai ...

Displaying Firebase data using Angularfire2 5.0 on an Ionic template

Hey everyone, I've been encountering a problem while trying to use angularfire2 v 5.0. I was comfortable using v 4.0 before, but now that I'm transitioning to v 5.0, I'm facing some issues. Does anyone know how I can display real-time data ...

Angular 4 - Sum all values within a nested array of a data model

I am working with an array of Models where each object contains another array of Models. My goal is to calculate the sum of all the number variables from the nested arrays using the code snippet below. Model TimesheetLogged.ts export interface Timesheet ...

Retrieve the predetermined value from the dropdown menu option

I currently have two classes with a mapping structure as follows: User *--------------------1 Sexe Users are listed in the file list-users.component.html. When selecting a user for modification, I am redirected to the modify-user.component.html B ...

I encountered a warning while using the useViewportScroll in NextJs with Framer Motion: "Caution: The useLayoutEffect function does not have any effect on the server

Successfully implementing NextJs with Framer Motion, yet encountered a warning: Warning: useLayoutEffect does not function on the server due to its effect not being able to be encoded in the server renderer's output format. This may cause a differenc ...

I encountered some problems with conflicting Angular dependencies, so I decided to delete the node_modules folder

An error has occurred: The module 'H:\All_Files\Scripts\Angular\example\node_modules\source-map\source-map.js' could not be found. Please ensure that the package.json file contains a correct "main" entry. ...

Verifying callback type in Typescript based on another argument's validity

There is a JavaScript function that I am working with: const fn = (cb, param) => { cb(param); }; This function is meant to be called in two ways within TypeScript: const cb0 = () => {}; fn(cb0); const cb1 = (param: string) => { }; fn(cb1, &a ...

The collaboration between Redux's combineReducers and the power of TypeScript

I'm facing a challenge with using react-intl-redux and redux-form in my react app, specifically when working with combineReducers. Despite trying multiple approaches, I haven't been able to resolve the issue. react-intl-redux import { combineRe ...

Tips for transforming a Json array into an object in Angular 5

I am working with a Json array that looks like this: [{"name":"ip","children":{"label":"ip","value":"","type":"text","validation":"{ required: true}"}} ,{"name":"test","children":{"label":"test","value":"","type":"text","validation":"{ required: true}"}} ...

Obtaining JSON data in React Native without prior knowledge of the key

When I receive this type of data, the keys are common but the items vary. How can I extract and add this data to my list of categories? { "99": "Venues", "100": "Party Supplies", "101": "Enter ...

The type (string | undefined) cannot be assigned to type string[] even when using the .filter function to remove undefined elements

Consider the following code: let someVar: Array<string>; somevar = ["a", "b", undefined, "c"].filter((it) => !!it); The code above is resulting in an error: Type '(string | undefined)[]' is not assignable t ...