5

TypeScript has convenient "parameter property" shorthand for class constructors so that I don't have to explicitly define every property, like this:

class Pizza {
  constructor(
    private sauce: string,
    private cheese: string,
    public radius: number
  ) {}
}

However, I can also use a constructor that takes an object rather than an argument list (essentially, keyword arguments):

const pizza = new Pizza({sauce: 'marinara', cheese: 'Venezuelan Beaver', radius: 8})

This syntax has a number of conveniences, not least of which is the ability to use the spread operator to easily copy objects:

const anotherPizza = new Pizza({...pizza, radius: 10})

But unfortunately it means I can't use the constructor shortcut to avoid all the boilerplate (I think):

class Pizza {

  private sauce: string;
  private cheese: string;
  private radius: number;

  constructor({sauce, cheese, radius}: {
      sauce: string,
      cheese: string,
      radius: number
  }) {
    this.sauce = sauce;
    this.cheese = cheese;
    this.radius = radius;
  }
}

Is there some way to use an object-literal constructor like this and still avoid assigning every property explicitly?

3
  • No. The closest you could get is spreading an array (or tuple, type-wise) of values. Commented Aug 19, 2022 at 13:59
  • @jonrsharpe Could you show a brief example of that? I'm still not sure how I can use the parameter property syntax with an array argument. Commented Aug 22, 2022 at 16:07
  • It wouldn't change the class definition or parameter property syntax at all, the point is you'd spread an array e.g. ['marinara', 'Venezuelan Beaver', 8] to it, instead of an object. Commented Aug 22, 2022 at 16:08

2 Answers 2

1

The closest thing to what you're looking for is to get a tuple of constructor arguments using the relevant utility type. You can then create an array of parameter arguments and spread them (type-safely) to the constructor:

class Pizza {
  constructor(
    private sauce: string,
    private cheese: string,
    public radius: number
  ) {}
}

type PizzaSpec = ConstructorParameters<typeof Pizza>;

const spec: PizzaSpec = ["marinara", "Venezuelan Beaver", 8];

const pizza = new Pizza(...spec);

Playground

Although you can't use an object with property keys, the type you get is a labeled tuple, equivalent to writing:

const spec: [sauce: string, cheese: string, radius: number] = ...;

so you do get a little more documentation than just [string, string, number].

Copying is much more limited, though; dealing with array slices in a type-safe way is pretty complicated.

Sign up to request clarification or add additional context in comments.

Comments

0

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).

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.