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 filter
ing 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