Designing a TypeScript class with unique properties and attributes

I have an idea for creating a versatile class named Model. Here's what I have in mind:

export class Model {
    _required_fields: Array<string> = [];
    _optional_fields?: Array<string> = [];

    constructor(params: Dictionary<string> = {}) {
        // Ensure all required fields are present in the params object
    }

    set(params: Dictionary<string>){
        // Verify that only required or optional fields are included

        this.all_fields.forEach(key => {
            this[key] = params[key];
        });
    }

    get all_fields(): Array<string> {
        return [...this._required_fields,...this._optional_fields];
    }

    get required_fields() {
        return this._required_fields;
    }
}

Children of this class will specify the required and optional fields. I've kept the code concise by implementing error checking in the set method. For instance:

export class User extends Model {
    static REQUIRED_FIELDS = ['username'];
    static OPTIONAL_FIELDS = ['email'];

    get required_fields() {
        return (this._required_fields?.length==0) ? User.REQUIRED_FIELDS : [];
    }

    static get ALL_FIELDS() {
        return [...User.REQUIRED_FIELDS, ...User.OPTIONAL_FIELDS];
    }

    constructor(params: Dictionary<string> = {}) {
      super(params);
    }
}

I have defined a version of User with fields:

username: string;
email: string;

However, I want to be able to define the fields dynamically so that the set function can populate them based on a Dictionary.

The TypeScript error I'm encountering is

No index signature with a parameter of type 'string' was found on type 'Model'
at the line:

this[key] = params[key];

This issue arises because I need to include a field like: [key: string]: string; within the Model definition.

There appear to be two potential solutions for this problem:

Method 1: Explicitly defining each field inside of User, and manually assigning values within the set method (of User) as follows:

user.username = params.username;
user.email = params.email;

Although this approach works, it involves repetitive coding for all children of Model, which contradicts my goal of automating some error checks.

Method 2: Alternatively, retaining a generic field within the Model:

[key: string]: string;

This allows the existing set implementation to function without direct access to fields such as user.username, but rather user['username'].

Summary

Thus far, I've followed Method 1, resulting in extensive duplicated code across child classes of Model. This repetitiveness isn't ideal, considering I may have multiple fields to manage. Is there a more efficient way to structure this logic within the Model class itself?

Though Method 2 offers brevity, it sacrifices some of TypeScript's strong typing capabilities. While compact, this compromise doesn't seem optimal.

Question

Is there a middle ground where I can combine TypeScript's robust typing with the convenience of Method 1?

Answer №1

If your intention is to track specific values in elements such as requiredFields and optionalFields within a Model, it might be advantageous to make the Model more generic in these types. One approach could be structuring it as Model<R, O>, where R includes all essential keys and O covers optional keys.

However, incorporating type R and keys of O into a Model<R, O> can pose challenges with standard class declarations. Classes and interfaces require known keys statically to the compiler and do not support dynamic or late filling:

class Foo<T extends object > ; implements T {} // error!
// -----------------------------------> ~
// A class can only implement an object type or intersection 
// of object types with statically known members.

Hence, finding a workaround becomes crucial.


A practical method involves defining a class constructor type like so:

type ModelConstructor = new <R extends string, O extends string>(
    params: ModelObject<R, O>
) => ModelObject<R, O> & {
    set(params: Partial<ModelObject<R, O>>): void,
    all_fields: Array<R | O>;
    required_fields: R[];
}

type ModelObject<R extends string, O extends string> =
    Record<R, string> & Partial<Record<O, string>>;

The construct signature within ModelConstructor accepts a params of type ModelObject<R, O>. This ModelObject<R, O> defines an object type with mandatory keys R and optional keys

O</code, where all values are strings. The instance created aligns with <code>ModelObject<R, O>
intersected with properly typed properties and methods inside your Model class.

Prior to proceeding further, consider how to subclass Model. If each instance of, for example, User, requires the same required and optional fields, converting Model into a class factory function instead of a class constructor may be beneficial. Otherwise, you would need to redundantly specify R and

O</code:</p>
<pre><code>class AnnoyingUser extends Model<"username", "email"> {
    _required_fields = ["username"] // redundant
    _optional_fields = ["email"] // redundant
}

An alternate method that streamlines this process is:

class User extends Model(["username"], ["email"]) { }

In this scenario, Model(["username"], ["email"]) constructs a preloaded class with those specified fields. The decision to adopt this approach is subjective. Assuming acceptance of a factory function, let's continue using it.


Here is the implementation of the factory function:

export const Model = <R extends string, O extends string>(
    requiredFields: R[], optionalFields: O[]
) => {

    type ModelObject =
        Record<R, string> & Partial<Record<O, string>> extends
        infer T ? { [K in keyof T]: T[K] } : never;

    class _Model {
        _required_fields: Array<R> = requiredFields;
        _optional_fields?: Array<O> = optionalFields;

        constructor(params: ModelObject) {
            this.set(params);
        }

        set(params: Partial<ModelObject>) {
            this.all_fields.forEach(key => {
                (this as any)[key] = (params as any)[key];
            });
        }

        get all_fields(): Array<R | O> {
            return [...this._required_fields,
            ...this._optional_fields ?? []];
        }

        get required_fields() {
            return this._required_fields;
        }
    }

    return _Model as any as new (params: ModelOb...</answer1></answer1>
<exanswer1><div class="answer accepted" i="69597918" l="4.0" c="1634393867" m="1634409966" v="1" a="amNhbHo=" ai="2887218">
<p>If you are looking to configure <code>Model to oversee the explicit values in elements like requiredFields and optionalFields, customizing Model to be generic in these types could be beneficial. Consider utilizing a structure like Model<R, O>, where R merges all essential keys and O</code encompasses the optional keys.</p>
<p>However, ensuring that a <code>Model<R, O> effectively includes type R and keys of O</code poses challenges with traditional <code>class declarations. Classes and interfaces necessitate known keys statically to the compiler and cannot allow for dynamic or late filling:

class Foo<T extends object > implements T {} // error!
// -----------------------------------> ~
// A class can only implement an object type or intersection 
// of object types with statically known members.

Therefore, finding a workaround is crucial.


One practical approach involves defining a class constructor type as follows:

type ModelConstructor = new <R extends string, O extends string>(
    params: ModelObject<R, O>
) => ModelObject<&g...ny necessary static properties of the returned class constructor.  I am not going to delve into that.  Just food for thought.</p>
<hr />
<p>Let's give it a try. First, let's see what happens when we call <code>Model:

const UserModel = Model(["username"], ["email"]);
/* const UserModel: new (params: {
    username: string;
    email?: string | undefined;
}) => {
    username: string;
    email?: string | undefined;
    set: (params: Partial<{...
} */

Seems reasonable and aligned with your requirements. Next, we can define the User class:

export class User extends Model(["username"], ["email"]) {
    // static REQUIRED_FIELDS = ['username'];
    // static OPTIONAL_FIELDS = ['email'];
}

The commented out static properties were from your initial example, but they are unnecessary in my solution. You can always include them back if needed, although their purpose remains unclear.

Now, let's test it out:

const u = new User({ username: "alice" });
console.log(u.required_fields); // ["username"]
console.log(u.all_fields); // ["username", "email"]
u.set({ email: "<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="20646d6d52677a636f727e774c010d0f">[email protected]</a>" });
console.log(u.email); // <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="3e7c757552777a63655b64665741647b53570b6656585a">[email protected]</a>

Looks good!

Playground link to code

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

Modifying the text of a material UI button depending on a particular condition

I have a component that uses the Material UI button, and I need to change the text of the button based on a condition. If the order amount is 0, I want it to display "cancel", otherwise, it should show "refund". Can someone guide me on how to achieve thi ...

Using Jasmine to Jest: Mocking Nested function calls

I am currently working on testing my TypeScript functions with Jasmine: //AB.ts export async function A() { } export async function B() { A(); } My goal is to unit test function B by mocking out function A to see if it is called. Here is the code I h ...

Position the circles in a way that they align along the circumference of a larger

I need help arranging a group of circles around another circle without any of them overlapping. I have all the radius measurements for each circle, as well as the coordinates for the target circle. The target circle will always be large enough to contain ...

Restricting array elements through union types in TypeScript

Imagine a scenario where we have an event type defined as follows: interface Event { type: 'a' | 'b' | 'c'; value: string; } interface App { elements: Event[]; } Now, consider the following code snippet: const app: App ...

Tips for troubleshooting a Typescript application built with Angular 2

What is the best approach for debugging an Angular 2 Typescript application using Visual Studio Code or other developer tools? ...

Can you please provide an explanation on the functioning of Dependency Injection in Nestjs?

I have been delving into Nest.js and incorporating it into my project structure. Additionally, I have integrated TypeORM into the mix. The concept of Dependency Injection in Nest.js has me feeling a bit perplexed. Project Structure |-APP_MODULE |-app.co ...

The image map library functions seamlessly with React but encounters issues when integrated with Next.js

I have been working on a client project that requires the use of an image map. I searched for a suitable library, but struggled to find one that is easy to maintain. However, I came across this particular library that seemed promising. https://github.com/ ...

Comparing dates in Angular 6 can be done by using a simple

Just starting with angular 6, I have a task of comparing two date inputs and finding the greatest one. input 1 : 2018-12-29T00:00:00 input 2 : Mon Dec 31 2018 00:00:00 GMT+0530 (India Standard Time) The input 1 is retrieved from MSSQL database and the in ...

Why can't I omit <someUnion, oneInterface> in TypeScript?

Kindly review this simple example: interface A { a: number; } interface B { b: number; } interface C { c: number; } type ABC = A | B | C; type omitA = Omit<ABC, A>; https://i.sstatic.net/5Mun4.png Unfortunately, I am unable to exclude an i ...

Tips for eliminating unicode characters from Graphql error messages

In my resolver, I have implemented a try and catch block where the catch section is as follows: catch (err: any) { LOG.error("Failed to get location with ID: " + args.id); LOG.error(err); throw new Error(err); ...

Angular 6 allows for the use of radio buttons to dynamically enable or disable corresponding fields

Here is the HTML code for a row containing radio button selections: <div class="form-row"> <div class="col-md-3 mb-3"> <div class = "form-group form-inline col-md-12 mb-3"> <div class="form-check form-check-inl ...

What are the steps to resolving an issue in a Jest unit test?

In my ReactJs/Typescript project, I encountered an issue while running a unit test that involves a reference to a module called nock.js and using jest. Initially, the import statement was causing an error in the .cleanAll statement: import nock from &apos ...

Searching for MongoDB / Mongoose - Using FindOneById with specific conditions to match the value of an array inside an object nestled within another array

Although the title of this question may be lengthy, I trust you grasp my meaning with an example. This represents my MongoDB structure: { "_id":{ "$oid":"62408e6bec1c0f7a413c093a" }, "visitors":[ { "firstSource":"12 ...

Managing errors when dealing with Observables that are being replayed and have a long lifespan

StackBlitz: https://stackblitz.com/edit/angular-ivy-s8mzka I've developed an Angular template that utilizes the `async` pipe to subscribe to an Observable. This Observable is generated by: Subscription to an NgRx Store Selector (which provides sele ...

Adjusting the transparency of TabBadge in Ionic 2

I am currently working on a project that involves tabs, and I'm looking to update the style of the badge when the value is 0. Unfortunately, I am unsure how to dynamically change the style of my tabs or adjust the opacity of the badge in the style. M ...

Unable to connect to web3 object using typescript and ethereum

Embarking on a fresh project with Angular 2 and TypeScript, I kicked things off by using the command: ng new myProject Next, I integrated web3 (for Ethereum) into the project through: npm install web3 To ensure proper integration, I included the follow ...

Issue TS1192: The module named "A.module" does not contain a default export

After creating a new module 'A', I attempted to import it in another module named 'B'. However, during compilation, I encountered the following error: Error TS1192: Module '" A.module"' has no default export I wou ...

Chaining Assignments in TypeScript

let a: { m?: string }; let b = a = {}; b.m = ''; // Property 'm' does not exist on type '{}'. let a: { m?: string } = {}; let b = a; b.m = ''; // It's OK Link to TypeScript Playground What occurs ...

tsc does not support the use of the '--init' command option

Encountering an error when running npx tsc --init: $ npx tsc --init npx: installed 1 in 1.467s error TS5023: Unknown compiler option 'init'. I've added the typescript package using Yarn 2: $ yarn add -D typescript ➤ YN0000: ┌ Resolution ...

How to disable typescript eslint notifications in the terminal for .js and .jsx files within a create-react-app project using VS Code

I'm currently in the process of transitioning from JavaScript to TypeScript within my create-react-app project. I am facing an issue where new ESLint TypeScript warnings are being flagged for my old .js and .jsx files, which is something I want to avo ...