Creating a dynamic type class in TypeScript that can inherit characteristics from another class

Can a wrapper class be designed to dynamically inherit the properties of another class or interface? For instance...

interface Person {
    readonly firstName: string;
    readonly lastName: string;
    readonly birthday?: Date
}

class Wrapper<T> {
    public constructor(template: T) {
        ...
    }

    public foo(): void {
        ...
    }

    public bar(): void {
        ...
    }
}

const p: Wrapper<Person> = new Wrapper({
    firstName: "John",
    lastName: "Smith"
});

p.foo() // works fine
p.bar() // works fine
p.firstName // compiler error, property does not exist in Wrapper<T>

Is it possible to inform the compiler that Wrapper<T> should also include the properties of T?

Answer №1

One possible solution is to implement a factory method, like this:

function createWrapper<T>(data: T): Wrapper<T> & T {
  return new Wrapper(data) as Wrapper<T> & T;
}

const w = createWrapper<WidgetInfo>({
  name: 'Widget 1',
  color: 'blue'
});

w.display(); // okay
w.name; // okay

Keep in mind that there may be challenges when dealing with additional fields from the data object within your class implementation. It's important to consider how this might affect your specific scenario.

Answer №2

To achieve this, you can utilize the provided code snippet available at the following playground link.

class Wrapper<T> {
    static create<T>(template: T): Wrapper<T> & T {
        const target = new Wrapper(template);
        const source = template as any;
        Object.keys(template).forEach(key => {
            Object.defineProperty(target, key, {
                get: () => source[key],
                set: value => { source[key] = value }
            })
        });

        return target as any;
    }

    private _template: T;

    private constructor(template: T) {
        this._template = template;
    }

    foo(): void { console.log('foo'); }
    bar(): void { console.log('bar'); }
}

interface Person {
    readonly firstName: string;
    readonly lastName: string;
    readonly birthday?: Date
}

// with explicit generic
const wrappedPerson = Wrapper.create<Person>({
    firstName: "John",
    lastName: "Smith"
});

wrappedPerson.foo();
console.log(wrappedPerson.firstName, wrappedPerson.lastName, wrappedPerson.birthday);

// with type inference 
const wrappedObject = Wrapper.create({
    firstName: "Al",
    lastName: "Green"
});

wrappedObject.bar();
console.log(wrappedObject.firstName, wrappedObject.lastName);

The above code demonstrates the create method creating dynamic accessors on the wrapper object by using Object.defineProperty, which then delegates to the defined template.

Below is the corresponding JavaScript version:

class Wrapper {
    constructor(template) {
        this._template = template;
    }
    static create(template) {
        const target = new Wrapper(template);
        const source = template;
        Object.keys(template).forEach(key => {
            Object.defineProperty(target, key, {
                get: () => source[key],
                set: value => { source[key] = value; }
            });
        });
        return target;
    }
    foo() { console.log('foo'); }
    bar() { console.log('bar'); }
}
// with explicit generic
const wrappedPerson = Wrapper.create({
    firstName: "John",
    lastName: "Smith"
});
wrappedPerson.foo();
console.log(wrappedPerson.firstName, wrappedPerson.lastName, wrappedPerson.birthday);
// with type inference 
const wrappedObject = Wrapper.create({
    firstName: "Al",
    lastName: "Green"
});
wrappedObject.bar();
console.log(wrappedObject.firstName, wrappedObject.lastName);

Answer №3

To solve this issue, I made an update by introducing a union type between the wrapper and the template:

type Combined<T> = Wrap<T> & T;

Considering that the constructor for Wrap<T> can only yield Wrap<T>, I also added a static method to create instances of Combined<T>:

class Wrap<T> {
    constructor(template: T) {
        ...
    }

    public bar() { ... }

    public static fromTemplate(template: T): Combined<T> {
        return new Wrap<T>(template) as Combined<T>;
    }
}

const y: Combined<Individual> = Wrap.fromTemplate({
    name: "Alice",
    age: 28,
    occupation: "Engineer"
});

y.bar() // success
y.name // success

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

Incorporate personalized No Data Available message in ngx-datatable

How can I customize the no data message for ngx-datatable? I want to avoid displaying the default message that comes with it. Here is what I have attempted so far: <div *ngIf="showTable"> <ngx-datatable [rows]="rows"> ...

Issue accessing member value in inherited class constructor in Typescript

My situation involves a class named A, with another class named B that is inherited from it. class A { constructor(){ this.init(); } init(){} } class B extends A { private myMember = {value:1}; constructor(){ super(); ...

What is the mechanism behind the widening of object literal types in Typescript inference?

I've been reading up on how typescript broadens inferred types but I'm still not entirely clear about what's happening here: type Def = { 'T': { status: 5, data: {r: 'm'}}, } function route<S extends keyof Def> ...

What could be causing my Page to not update when the Context changes?

In my Base Context, I store essential information like the current logged-in user. I have a User Page that should display this information but fails to re-render when the Context changes. Initially, the Context is empty (isLoaded = false). Once the init fu ...

What is the best way to retrieve JSON data from a raw.github URL and save it into a variable?

Suppose there is a JSON file named data.json on Github. The raw view of the file can be accessed through a URL like this: https://raw.githubusercontent.com/data.json (Please note that this URL is fictional and not real). Assume that the URL contains JSON ...

Encountering ERR_INVALID_HTTP_RESPONSE when trying to establish a connection with a Python gRPC server using @bufbuild/connect

Attempting to establish a connection to a python grpc server developed with grpcio through a web browser using connect-query. Encountering an issue where upon making a request, an ERR_INVALID_HTTP_RESPONSE error is displayed in the browser. Below is the Re ...

Developing a constructor method that is conscious of data types

In my current scenario, I am dealing with a set of types: X, Y, and Z, all of which extend the same common interface J. My goal is to define a method that looks like this: class MyClass { private someNumber = 1; private someProperty; addEleme ...

Experiencing difficulties with the mat-card component in my Angular project

My goal is to implement a login page within my Angular application. Here's the code I've written: <mat-card class="login"> <mat-card-content> <div class="example-small-box mat-elevation-z4"> ...

Ways to verify if the current date exists within a TypeScript date array

I am trying to find a way in typescript to check if the current date is included in a given array of dates. However, even after using the code below, it still returns false even when the current date should be present within the array. Can anyone please pr ...

Utilizing symbols as a keyof type: A simple guide

Let's consider the following: type Bar = keyof Collection<string> In this scenario, Bar denotes the type of keys present in the Collection object, such as insert or remove: const x: Bar = 'insert'; ✅ But wait, the Collection also c ...

Error encountered when attempting to pass i18next instance to I18nextProvider

Issue: Error message: Type 'Promise' is missing certain properties from type 'i18n': t, init, loadResources, use, and more.ts(2740) index.d.ts(344, 3): The expected type is derived from the property 'i18n' declared within ty ...

Creating a View-Model for a header bar: A step-by-step guide

I am looking to develop a View-Model for the header bar using WebStorm, TypeScript, and Aurelia. In my directory, I have a file named header-bar.html with the following code: <template bindable="router"> <require from="_controls/clock"></ ...

A comparison between Buffer.byteLength and file size

I'm facing an issue with file size discrepancies. I have a file that is reported as 51Mb in Finder, but when uploaded to the server, the byteLength of the Buffer shows a much smaller size. Could this difference be due to the file type or other propert ...

Jest test encountering an issue where FileReader, File, and TextDecoder are not defined

While I have primarily used Jasmine for tests in the past, I am now experimenting with Jest. However, I have encountered an issue where classes like FileReader, File, and TextDecoder are not defined in my tests. How can I incorporate these classes with t ...

Unable to spy on the second and third call using Jest

I'm having trouble using spyOn on the second and third calls of a function in my jest test I attempted to follow the documentation with this approach: it("should succeed after retry on first attempt failure", async () => { jest.spyOn(n ...

Guide on importing SVG files dynamically from a web service and displaying them directly inline

With an extensive collection of SVG files on hand, my goal is to render them inline. Utilizing create-react-app, which comes equipped with @svgr/webpack, I am able to seamlessly add SVG inline as shown below: import { ReactComponent as SvgImage } from &apo ...

Suddenly encountered issue when working with TypeScript Interfaces while integrating Quicktype 'allOf'

Our transition from using JSON Type Definition to JSON Schema includes the utilization of Quicktype to convert JSON Schemas into TypeScript Types. While Quicktype has been effective in most cases, it seems to struggle with converting Discriminators and mor ...

The functionality of d3 transition is currently non-existent

I've encountered an issue with my D3 code. const hexagon = this.hexagonSVG.append('path') .attr('id', 'active') .attr('d', lineGenerator(<any>hexagonData)) .attr('stroke', 'url(#gradi ...

What is preventing the spread type from being applied to `Record` in TypeScript?

export type AddResourceProps<K extends string, T extends any> = (resource: BasicResource) => Record<K, T> const addtionalResourse = addResourceProps ? addResourceProps(resource) : {} as Record<K,T> const result = { ...addtionalRe ...

Struggling to translate JavaScript code into Typescript

Currently in the process of converting my JavaScript code to Typescript, and encountering an error while working on the routes page stating Binding element 'allowedRoles' implicitly has an 'any' type. ProtectedRoutes.tsx const Protecte ...