1

I want to modify the Blog-constructor from "Minimal working example" below in a way that is described as "pseudo-code". How to initialize the members of class "Blog" via generic ModelHelper class, so that I can rewrite the Blog constructor as:

pseudocode:

constructor(data: Partial<IModel> = {}) {
  ModelHelper.initMembers(this, data, initialBlogConfig);
}
export class ModelHelper {
  static initMembers<T, U>(model: T, data: Partial<IModel>, initialModel: U): void {
    for (key in initModel) {
      if(data[key] !== null && data[key] !== undefined) {
        model[key] = data[key];
      }else{
        model[key] = initialModel[key]
      }
    }
  }
}

"Minimal working example":

interface IBlog {
  title: string;
  content: string;
}
const initialBlogConfig: IBlog = {    
  title: "",
  content: "",  
};
interface IModel {
  [key: string]: any;
}

class Blog implements IBlog {
  title: string;
  content: string;

  constructor({title, content}: Partial<IModel> = {}) {
    this.title = title ?? "";
    this.content = content ?? "";
  }
}
Hölderlin
  • 424
  • 1
  • 3
  • 16

1 Answers1

1

If you want to write a helper function to handle all aspects of property initialization for you, instead of using an initializer or directly assigning the properties inside the constructor, then the compiler won't be able to tell that the properties were initialized and you'll need to use a definite assignment assertion (!) to suppress errors:

class Blog implements IBlog {
    title!: string;
    content!: string;
    constructor(data: NullableProps<IBlog> = {}) {
        ModelHelper.initMembers(this, data, { title: "", content: "" });
    }
}

let blog = new Blog({ title: "test", content: null })
console.log(blog); // {title: "test", content: ""}

where NullableProps<T> is a utility type of the form

type NullableProps<T> = { [K in keyof T]?: T[K] | null };

so that all properties are optional (so they can be missing or undefined) and nullable (so they can be null also).


If so then you can implement initMembers() in any number of ways. Here's one way:

const ModelHelper = {
    initMembers<T extends object>(
        model: T, data: NullableProps<T>, initialConfig: T
    ): void {
        Object.assign(model,
            initialConfig,
            Object.fromEntries(
                Object.entries(data).filter(([_, v]) => v != null)
            )
        );
    }
}

You could write that as a class with a static method if you really want to, but if you're not going to be using the class's constructor it's not necessary (like in Java) or particularly useful to do so.

The implementation above works by first copying all the properties from initialConfig into model, using the Object.assign() method, and then by copying all the non-nullish properties from data into model (which were obtained by filtering the entries obtained from Object.fromEntries() and converting back via Object.fromEntries()).

You can use for loops if you want, but you need to be careful to get all the keys from both initialConfig and from data, in case the generic T type of model has any optional properties:

class Example {
    req1!: string;
    req2!: number;
    opt1?: boolean;
    opt2?: string;
    constructor(data: NullableProps<Example> = {}) {
        ModelHelper.initMembers(this, data, { req1: "a", req2: 1, opt1: false })
    }
}
let example = new Example({ opt2: "b" })
console.log(example) // {req1: "a", req2: 1, opt1: false, opt2: "b"}

Any implementation would need to make sure to copy both opt1 and opt2 appropriately.


On the other hand, if you don't mind using a field initializer to handle the initialConfig defaults, then you can forgo the assertions and just have initMembers handle the partial input:

class Blog implements IBlog {
    title = "";
    content = "";
    constructor(data: NullableProps<IBlog> = {}) {
        ModelHelper.initMembers(this, data);
    }
}

let blog = new Blog({ title: "test", content: null });
console.log(blog); // {title: "test", content: ""}

const ModelHelper = {
    initMembers<T extends object>(
        model: T, data: NullableProps<T>
    ): void {
        Object.assign(model,
            Object.fromEntries(
                Object.entries(data).filter(([_, v]) => v != null)
            )
        );
    }
}

This is easier to get right with a for loop, since there's only the data argument to worry about, so you only need to iterate over its keys:

const ModelHelper = {
    initMembers<T extends object>(
        model: T, data: NullableProps<T>
    ): void {
        for (const [k, v] of Object.entries(data) as Array<[keyof T, T[keyof T]]>) {
            if (v != null) model[k] = v;
        }
    }
}

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Thanks for your detailed answer. Last question, why there is no explicit check if `...&& v != undefined` or why does `new Blog({ title: "test", content: undefined });` not sets content to `undefined`? In this context is `new Blog({ title: "test", content: undefined });` same as `new Blog({ title: "test" });`? – Hölderlin Aug 07 '23 at 05:38
  • 1
    Note that it's `!=` and not `!==`. Writing `v != null` checks for both `null` and `undefined` because of the behavior of the loose inequality operator. See [this question](https://stackoverflow.com/q/2647867/2887218). – jcalz Aug 07 '23 at 12:49