IMHO the copy idea is flawed in this context: When you use the new operator, you create a new object anyway and the fields are (only) initialised in the constructor.
That is
const anotherPizza = new Pizza({...pizza, radius: 10})
actually creates a new object (almost a copy of pizza), then creates another object anotherPizza and sets its fields to the (slightly modified) copy of pizza. This is a performance nightmare as you do not need the intermediate copy, although I would hope that V8 and other engines would detect the use case and omit the superfluous copy.
Besides that, I like your idea and I had similar issues. When the number of fields grow, code becomes much more readable when using named arguments by means of an object literal. So, what I was looking for is something like that:
class Pizza {
constructor({
private sauce: string,
private cheese: string,
private radius: number
}) {
}
}
which does not work of course.
Unfortunately, I'm afraid your example cannot be optimised (by means of shortened) since you are using private fields: Using mapped types does not work with private fields as the keyof type operator only works on public fields (cf. TS issue 46802).
If all your fields are public, you could use the following construct:
type Fields<T> = {
[Property in keyof T as T[Property] extends Function ? never : Property]: T[Property]
};
class Pizza {
sauce: string;
cheese: string;
radius: number;
constructor(src: Fields<Pizza>) {
this.sauce = src.sauce
this.cheese = src.cheese
this.radius = src.radius
}
}
That is, you do not need to list all the fields in the type of the parameter of the constructor, and you also get error messages if you add or remove fields. I sometime use this pattern when converting an interface or type to a class. As you can see I also do not use destructering so that I do not have to write the fields in the signature at all.
In order to filter out readonly properties (getters), you need to add another filter type as described in another stackoverflow thread:
// Part 1: Filter out readonly properties
// cf. https://stackoverflow.com/questions/52443276/how-to-exclude-getter-only-properties-from-type-in-typescript/52473108#52473108
type IfEquals<X, Y, A, B> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? A : B;
type WritableKeysOf<T> = {
[P in keyof T]: IfEquals<{ [Q in P]: T[P] }, { -readonly [Q in P]: T[P] }, P, never>
}[keyof T];
type Writeable<T> = Pick<T, WritableKeysOf<T>>
And then the constructor signature looks like this:
constructor(src: Writeable<Fields<MyClass>>)
Back to the copy idea: Maybe it would make sense to rewrite the code:
const anotherPizza = new Pizza(pizza)
anotherPizza.radius = 10;
Or another constructor:
constructor(src: Fields<PizzaShorter>, changes?: Partial<Fields<PizzaShorter>>) {
this.sauce = changes?.sauce ?? src.sauce;
this.cheese = changes?.cheese ?? src.cheese;
this.radius = changes?.radius ?? src.radius;
}
However I'm not sure whether this is really worth the trouble due to JavaScript engine optimisation (this has to answer someone with more V8 insight knowledge).
['marinara', 'Venezuelan Beaver', 8]to it, instead of an object.