Typescript: Combining destructuring with parameter properties

Created on 19 Oct 2015  ·  70Comments  ·  Source: microsoft/TypeScript

Today, we can take advantage of parameter properties to reduce the boilerplate, e.g:

class Person {
  constructor(public firstName: string, public lastName: number, public age: number) {

  }
}

Since 1.5, we can also use destructuring, e.g:

class Person {
  firstName: string;
  lastName: string;
  age: number;

  constructor({ firstName, lastName, age } : { firstName: string, lastName: string, age: number }) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
  }
}

I've tried in many ways to combine both features, but had no success. So:

  1. Is it possible to combine them nowadays, and if yes, how?
  2. If not, could it be an improvement to a future TypeScript version? E.g:
class Person {
  constructor(public { firstName, lastName, age } : { firstName: string, lastName: string, age: number }) {

  }
}

// the code above would possibly transpile to:
var Person = (function () {
    function Person(_a) {
        var firstName = _a.firstName, lastName = _a.lastName, age = _a.age;
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }
    return Person;
})();
Awaiting More Feedback Suggestion

Most helpful comment

Accepting PRs to implement constructor(public {name1, name2}) forms (i.e. no public / private etc _inside_ the {}s).

All 70 comments

I like this.

@buzinas I originally had this working in #1671, but didn't take it in (check out #1541). I think the issue was that it was relatively dense in semantics. I don't necessarily agree, but maybe someone else on the team can weigh in.

Note that in those proposals the parameter property modifier applied to the entire binding pattern (simpler) as opposed to the original suggestion here which in theory supports different visibility modifiers per destructured element (not sure that level of specificity would be worth it).

@danquirk For me, if it's easier for you to do the other way, I don't really care. In fact, that was my first try (e.g public {firstName, lastName, age}), and as soon as it didn't work, I tried to use on each property, and it didn't work too.

It would be great if we could support both (since not always we want to create properties for all the parameters, and when we want, it would be simpler to use public/private only once), but if it's easier to support only one approach, it will be great already.

Probably it's something that people will use more and more, since destructuring is awesome.

just ran into this. I think either approach would satisfy most use cases. Hope to see this in a future version.

The first thing I tried was:

  constructor(thisProps: { public myProperty: string, public myOtherProperty: number }) {
    // etc
  }

Something like this would be a nice-to-have.

Definitely +1 this. The alternative tends to be... bulky.

:+1:

:+1:

+1

+1

+1

👍

+1

Please use the GitHub reactions feature rather than standalone upvote comments. Thanks!

In an attempt to be more DRY using named args and strong types (until something like your proposal lands), I tried this:

interface ExampleArgs {
  firstArg: string;
  otherArg: number;
}

export default class Example implements ExampleArgs {
  firstArg;
  otherArg;

  constructor(kwargs:ExampleArgs) {
    return Object.assign(this, kwargs);
  }
}

but got Member 'firstArg' implicitly has an 'any' type. errors for every argument. ☹️

Write this instead

interface ExampleArgs {
  firstArg: string;
  otherArg: number;
}

export default class Example {
  constructor(kwargs:ExampleArgs) {
    return Object.assign(this, kwargs);
  }
}
export interface Example extends ExampleArgs { }

Thanks. I had to separate the exports from the declarations to make that work:

interface ExampleArgs {
  firstArg: string;
  otherArg: number;
}

class Example {
  constructor(kwargs:ExampleArgs) {
    return Object.assign(this, kwargs);
  }
}

export default Example;
interface Example extends ExampleArgs { }

This would be incredibly useful for hydrating class-based models from JSON.

As in

export interface PersonDto {
    name?: string;
}

export class Person {
    constructor(public {name}: PersonDto = {}) {
    }
}

Meanwhile we get this feature, here's the workaround:

export class PersonSchema {
    firstName: string;
    lastName: string;
    email?: string; // Thanks to TypeScript 2, properties can be optional ;)
}

export class Person extends PersonSchema {

    constructor(args: PersonSchema = {}) {
        Object.assign(this, args);
    }

}

The side effect is that if args has extra fields they will be copied into your new object.

This will also be your copy constructor.

Accepting PRs to implement constructor(public {name1, name2}) forms (i.e. no public / private etc _inside_ the {}s).

@RyanCavanaugh, you're proposing that

constructor(public { name1 = value1, name2 = value2 }: { name1: string, name2: string }) {}

would desugar to

constructor({ name1 = value1, name2 = value2 }) {
  this.name1 = name1;
  this.name2 = name2;
}

right?

Why is this discussion only about constructors? Wouldn't this apply to other methods as well?!?

It can't apply to other methods as only the constructor can declare
properties.
On Fri, 2 Dec 2016 at 08:54, Dirk Möbius notifications@github.com wrote:

Why is this discussion only about constructors? Wouldn't this apply to
other methods as well?!?


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/Microsoft/TypeScript/issues/5326#issuecomment-264394882,
or mute the thread
https://github.com/notifications/unsubscribe-auth/ACjP4sBomOO9wSXlIcw9NiDGf9-gZ2smks5rD86jgaJpZM4GRcCk
.

+1

So, will this ever be implemented?

class Person {

    firstName: string
    lastName: string
    age: number

    constructor(_: Person) {
        Object.assign(this, _)
    }
}

class PersonWithEmail extends Person {

    email: string

    constructor(_: PersonWithEmail) {
        super(_)
    }
}

let p = new PersonWithEmail({
    firstName: '',
    lastName: '',
    age: 0,
    email: '',
})

@andraaspar I'd imagine you'd run into issues if there were methods on Person.

const model = {
  firstName: 'Andras',
}

class Person {
  firstName: string;
  doAThing() {
  }
}

const thisShouldError: Person = model;

@appsforartists Sure you would. Not a replacement for the real thing. But it's elegant for model objects, until it gets implemented.

Hi, personally I'm using a Schema interface to avoid issues with methods etc... there's an example here: https://blog.wishtack.com/2017/05/06/angular-2-components-communication-using-reactive-stores/

The problem with Object.assign is that you might end up with dynamically added properties.
Example:

let data = {firstName: 'Foo', superfluousProperty: 'something'};
let person = new Person(data);
console.log(person['superfluousProperty']) // 'something'

It would be nice to be able to use keyof to generate a runtime array of class properties but this is not available for the moment. There are also some experimental approaches like this one https://github.com/kimamula/ts-transformer-keys.
Anyway, I'm sure, we'll get something nice soon :)

@yjaaidi this example fails, since some of the properties aren't optional. Just for people who are copying and pasting-

export class PersonSchema {
    firstName: string; //ERROR 
    lastName: string; //ERROR
    email?: string; // Thanks to TypeScript 2, properties can be optional ;)
}

export class Person extends PersonSchema {

    //ERROR firstName is not an optional property
    constructor(args: PersonSchema = {}) {
        Object.assign(this, args);
    }

}

however this works:

export class PersonSchema {
    firstName: string; 
    lastName: string; 
    email?: string; // Thanks to TypeScript 2, properties can be optional ;)
}

export class Person extends PersonSchema {

    constructor(args: PersonSchema = {firstName:"John",lastName:"Doe"}) {
        Object.assign(this, args);
    }

}

Really want this!

Another take using Pick to make some props mandatory:

type PersonOptions = Partial<Person> & Pick<Person, 'firstName' | 'lastName'>;

class Person {
  firstName: string;
  lastName: string;
  age: number = 0;

  constructor(options: PersonOptions) {
      for (const key in options) {
        this[key] = options[key]
    }
  }
}

const person = new Person({
    firstName: 'first',
    lastName: 'last',
    age: 27
})

const personeWithNoAge = new Person({
    firstName: 'first2',
    lastName: 'last2'
})

It seems I get full autocomplete! Don't use Object.assign(this, ...) since it does not allow correct autocomplete!

Extending my previous example with inheritance:

type SuperPersonOptions = Partial<SuperPerson> & Pick<SuperPerson, 'superProperty' | 'firstName' | 'lastName'>;

class SuperPerson extends Person {

    superProperty: string;
    optionalSuperProperty: string = '';

    constructor(options: SuperPersonOptions) {
        super({
            firstName: options.firstName,
            lastName: options.lastName
        })
        for (const key in options) {
            this[key] = options[key]
        }
    }
}

const superPerson = new SuperPerson({
    superProperty: 'im super',
    firstName: 'superFirst',
    lastName: 'superLast'
})

@chriszrc we don't have to suffer typing all of that text, let machines do that for us: https://constructor.stackblitz.io

@RyanCavanaugh are you still accepting PRs for this issue? :D I would like to work on this.

Just wanted to throw in my +1! This would be super useful. I'm guessing it's not much of a priority though being that this very issue was opened in 2015 :(

This would be extremely usefull for immutable classes:

class Immutable {
  constructor({
    public readonly fun: boolean,
    private readonly cool: boolean,
  }) { }

  get isCool(): boolean {
    return this.cool
  }
}
const immutable = new Immutable({fun: true, cool: true})

After spending some time with dart I started to value immutable objects and named constructors params.
Really think it's a nice idea.

Hi @ianldgs!

That's exactly the perfect pattern that needs this feature.
We also use it for "Entity" constructors as describe in our Angular guide here https://guide-angular.wishtack.io/typescript/duck-typing-patterns/entity-constructor (It's in french for the moment but google translate does a decent job).

Just going to leave this here for anyone to use. I've noticed that many of these options above are pretty verbose, when Typescript does some of the work for you. For example, you do not need to declare the type of each parameter if the name matches the object property. I believe the type is inferred (correct me if I'm wrong, but that is what I've noticed).

class Person
{
    _firstName: string;
    _lastName: string;
    _middleName?: string;

    constructor(data?: any = {}) 
    {
        this.firstName = data.firstName || 'Joe';
        this.lastName = data.lastName || 'Schmoe';
        this.middleName = data.middleName;    
    }

    // using setters for better control, hence no underscores when setting data in the constructor..
}

Extending @andraaspar 's example to allow for methods:

class Person {   
    firstName: string;
    lastName: string;
    age: number;

    constructor(_: NonFunctionProperties<Person>) {
        Object.assign(this, _);
    }

    fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
}

It also works well with default values for fields:

class Person {   
    firstName = "";
    lastName = "";
    age = 0;

    constructor(_?: NonFunctionProperties<Person>) {
        Object.assign(this, _);
    }
}

NonFunctionProperties<T> is defined in the 2.8 release notes as:

type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;

This also solves the problem with superfluous fields pointed out by @yjaaidi . It also works fine if some properties are optional; they also become optional in the constructor argument. On the downside, this approach doesn't play well with getters, which appear as non-functions to the type system.

@acchou

Cool example, but a couple deficiencies:

  • All non-function properties are required. TS will require you to provide all of them (or an arbitrary subset if you use Partial). This could make API evolution more difficult, because it would make existing callsites invalid if you ever added a new property (unless you replaced this pattern with an explicit interface).
  • It doesn't ignore readonly, which means it will require you to pass a value for any readonly parameter, even though they can't be written to.

@appsforartists If the properties are optional, then they won't be required in the constructor argument. Invalidating callsites when non-optional properties are added is a feature, not a bug.

There is a case where only a subset of properties have a default value. In that case the example I've given requires either all of the properties, or none of them (as in the second example). That's a deficiency that seems harder to address right now because "having a default value" is not part of the property's type.

I don't see the issue with readonly as we're talking about constructor arguments. readonly properties need to be initialized too.

After playing a bit with the implementation it seems TypeScript will warn about properties potentially not being initialized in the constructor because it doesn't fully understand Object.assign. So this is mainly useful if you have default values, !, or ? on all properties:

class Person {   
    firstName = "default";
    readonly lastName!: string;
    age?: number;

    constructor(_: NonFunctionProperties<Person>) {
        Object.assign(this, _);
    }

    fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
}

+1 on this feature too.

I read all the comments, and I don't see what's the real show stopper with this syntax:

class Person {
  constructor({ public firstName, public lastName, private age }: { firstName: string, lastName: string, age?: number }) {}
}

The visibility of the properties is obvious and the requirement of a variable can be based on the one set in the destructured object's type. The property requirement would be a little bit obscured, but it should't be game breaking since this feature would be so awesome to have as is.

I really don't like the Object.assign solution. It can fit as a temporary solution for some developers, but the intent is obscure.

And writing this.firstName = firstName; is way more frustrating when you know that a better solution is already implemented for non-destructured params.

Without this, I feel I have to chose between destructuring and in-param declaration if I want to keep my code readable. This is heartbreaking.

It's been 2 years since this issue was created. Let's not take another 2 years to decide if it should be done or not ;)

It's been 2 years since this issue was created.

3 years already. In the meantime I stopped using TypeScript at all, but I still receive notifications about this thread I created. I don't stop watching because I'm pretty curious to know when this will be solved.

How about extending the original proposition a bit to handle "default" values, like so:

class Person {
  constructor ({
    public name: string = 'John',
    public age: number = 21,
    public isEnrolled = true,      // <-- implicit type 
    private ssn: string,
  }) { }
}

and used like:
let p = new Person({name: 'Peter', ssn: '555-777-44'});

vs traditional:
let p = new Person('Peter', undefined, undefined, '555-777-44'); // <-- params not so obvious

@igpeev I think we'd all like to be able to put the key and type in the braces. Unfortunately, that conflicts with JavaScript's existing renaming syntax, so I doubt a solution that includes it will make much progress. (TypeScript is supposed to be a strict superset of JavaScript, not a fork.)

To someone who doesn't speak TypeScript, it looks like you're renaming name and ssn to string and age to number. How would the compiler know when to choose types vs. when to rename? How would someone rename a local property in your proposal?

'public'/'private' keywords could so nicely solve the dilemma, had they not been reserved for the 'future'.
P.S. Does anyone really use this renaming, would happily sacrifice it to have this constructor sugar

@appsforartists You are absolutely right.
@igpeev I don't think that typescript maintainers would sacrifice that.

But one workaround would be to prepend this: in the constructor:

class Immutable {
  constructor(this: {
    public readonly fun: boolean,
    private readonly cool: boolean,
  }) { }

  get isCool(): boolean {
    return this.cool
  }
}

:thinking: Clever!

There's already a meaning for this as an argument, but TypeScript bans that use in constructors, so this could be a solution.

(TypeScript is supposed to be a strict superset of JavaScript, not a fork.)

Maybe TypeScript should be forked then ;)

Seriously, I get that TypeScript maintainers want to keep typescript as close as possible to EcmaScript and I think it is the right choice, but it would be nice if we could create and share plugins to enable such features.

This isn't something that can be fixed via a language service plugin https://github.com/Microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin

Anyone know a way we could do it?

Not to derail this thread, but one of the great things about TypeScript (vs Babel) is that it's curated. Playing with Babel plugins is fun, but every library doesn't need its own dialect of the language.

That said, you can run Babel on top of TypeScript, which means you could probably author a Babel plug-in that would desugar whatever syntax you like into present-day TypeScript.

I am just saying that if TypeScript must be kept as close as possible to EcmaScript, having a way to create plugins would be a great way to augment it in a non-disruptive way.

That said, you can run Babel on top of TypeScript

I am not sure I like this idea. So far, TypeScript allowed me to avoid Babel and I am happy I don't have this extra layer of complexity into my environment.

Not to derail this thread

Well this thread has been open for more than three years. Maybe it needs to be derailed a little bit in order to get somewhere...

It's strange TypeScript still does not support something like this.
A great amount of code could be avoided easily.

I attempted an implementation for this feature a few months ago. I got stuck on some test cases, but this isn't as hard to do as you might imagine.

People in this thread who are passionate about this feature should give implementing it a go - the TypeScript team is accepting PRs for a reason! You'll probably get further than I did!

Current constructor parameters can make class declarations very hard to read since properties show up in two different sections (and indentations) of a class, and mixing up some proposal ideas and standard JS here we could end up with default values in 4 different places:

// this is not "real" code
// but what would go through the TS beginner's mind
// as they think of where properties and their default values should go...

class FooBar {
      foo:number = 1;
      constructor(this: { public foo: number = 2 } = { foo: 3 }) {
            this.foo = 4;
      }
}

No matter what restrictions we impose, any proposals for implementing destructuring in the constructor declaration will only lead to cluttered, hard to read code. Also, destructuring doesn't allow for transforming or coercion of initialization data.

There are 2 ways to go about it that would be more elegant.

One would be to add sugar to this nice, and currently supported, construct inspired by React's this.props:

type Properties<T> = Pick<
    T,
    { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]
>;

class FooBar {
    foo: number = this.props.foo * 10;
    bar?: number = this.props.bar;
    qaz?: number = this.props.qaz == null ? 33 : this.props.qaz;
    private distance : number;

    constructor(private props: Properties<FooBar>) {}
}

const foobar : FooBar = new FooBar({ foo: 11, bar: 22 });

I like the fact that:

  • constructed properties are clearly indicated by the = this.props.<property> idiom
  • constructor properties can be pre-processed and coerced during assignment, ie. = this.props.foo * 10
  • this.props remains around as a reference to the original properties, useful for object cloning or re-instantiation.
  • clearer and less global than using Object.assign()

And you can also easily add additional constructor-only parameters:

class FooBar {
    // ...
    bar?: number = this.props.baz * 10;
    constructor(private props: Properties<FooBar> & { baz? : number }) {}
}

If you prefer, put default values in the constructor instead of in the property initializer:

class FooBar {
     foo : number = this.props.foo;
     constructor(private props : Properties<FooBar> = { foo: 123 }) {}
}

It would be great to avoid the boilerplate type Properties ... (or NonFunctionProperties) every time. So any sugar in that direction would be great, ie a Properties, Props, NonFunction or similar core TS sugar that is concise and clear.

Also support for TC39 nullish coalescing ?? operator would make property initializers simpler:

class FooBar {
     public foo : number = this.props.foo ?? 123;
     constructor(private props);  // also don't require a constructor implementation
}

Now, if new syntax is to be introduced to properties, then it should be in the direction of convenient constructed properties, inspired by languages like Perl6:

Here's a proposal of a new construct keyword that would tell TS that the property will be built from the constructor object argument without the need for a constructor declaration/implementation:

class FooBar {
    construct private foo: number = this.foo * 10; // this.foo has value input by the user
    construct public bar?: number;
    private distance : number;
}

new FooBar({ foo: 11, bar: 22 });
new FooBar({ bar: 22 });  // Error: missing 'foo'
new FooBar({ foo: 11, distance: 33 });  // Error: 'distance' is not constructed

The constructor() method could still be available, but with a few limitations:

class FooBar {
      construct foo : number;
      bar:number;
      constructor(mystr: string) { } // Error: mismatched constructs
      constructor(private props) { }  // ok, props type is { foo: number }
      constructor(public props) { }  // Error TS2502, props is referenced directly or indirectly on its own type annotation
      constructor(props : any) { }  // Error: mismatched constructs
      constructor(props) {
            this.foo = this.foo || 99; // ok 
            this.bar = this.foo * 10;  // ok to keep building
      }
}

This would give developers great convenience and flexibility while being clear about intentions.

I found this works for me, let's me inject into constructors, or I can destructure and create:

export class Person{
  constructor(
    readonly firstName: string,
    readonly lastName: string,
    readonly email?: string,
  ) {}

  static new(args: Person) {
    return Object.assign(Object.create(Person.prototype), args)
  }
}

const person1 = new Person('first', 'last')
const person2 = new Person('first', 'last', '[email protected]')
const person3 = Person.new({ firstName: 'first', lastName:'last'})
const person4 = Person.new({ firstName: 'first', lastName: 'last', email: '[email protected]'})

Initially went with the schema and all args were deconstructed. But some tools like dexie lets you map class but needs it in the normal format. This gave me flexibility and type safety.

I have a class for incoming JSON objects with two known fields, id and owner.

It currently looks like this:

class DataItem {
  id: string;
  owner: string;
  constructor({
    id,
    owner,
    ...otherFields
  } : {
    id: string;
    owner: string;
    [others: string]: string | number | boolean;
  }) {
    this.id = id;
    this.owner = owner;
    Object.assign(this, otherFields);
  }
  // methods omitted
}

Would be great to have following simplified syntax using public that avoids specifying type of id and owner twice:

class DataItem {
  constructor({
    public id,
    public owner,
    ...otherFields
  } : {
    id: string;
    owner: string;
    [others: string]: string | number | boolean;
  }) {
    Object.assign(this, otherFields);
  }
  // methods omitted
}

Seems pretty important to me to support well classes ("method bags") for JSON-sourced objects which have some freedom of shape like in the above example.... @RyanCavanaugh, please consider.

NB: An alternative solution depending on and consistent with https://github.com/Microsoft/TypeScript/issues/5326#issuecomment-260505168 would be to allow public to be used with a rest parameter.

The above solution is great, it's compatible with ECMAScript and works well with current syntax. Unlike the public {...}: {...} destructing property, it also offers more possibilities, though that syntax would be nice to see too. Is anyone willing to implement this? Maybe I'd look into it this week.

Hi there! Any news on this?

Woooow. It's really amazing. I can't believe that ts still not implement this 'basic' language feature. Many js developers preferred named parameters at class constructor to make more clear codes. Auto assigning at constructor is great idea. Then, why do you guys not think that developers also want to use the auto assigning feature in named parameter way? It's been FIVE years! Many great ideas are already mentioned above.

This is probably not going to be implemented anytime soon. I think none of the proposals made everyone feel good. For now, I'm using a workaround based on a comment someone sent here previously but I can't find it to give the credits.
My main goal was to have immutable classes like dart, so private/protected doesn't really matter for me. And secondary was to support new MyClass(json)

interface IUser {
    readonly id: number
    readonly name: string
}

// merge the interface with the class
interface User extends IUser {}

class User {
    constructor(props: IUser) {
        Object.assign(this, props)
    }

    public copyWith(props: Partial<IUser>) {
        // spread operator only copies enumerable properties, so no need to worry about methods
        return new User({ ...this, ...props })
    }

    public sayHello() {
        console.log(this.name, 'says hello')
    }
}

const oldUser = new User({ id: 1, name: 'Foo' })
const newUser = oldUser.copyWith({ name: 'Bar' })

oldUser.sayHello() // "Foo says hello"
newUser.sayHello() // "Bar says hello"

oldUser.id = 2; // error: readonly
newUser.id = 2; // error: readonly

@ianldgs that's very good, with the available ways. Shortening it to the objective, we have:

interface IUser {
    id: number;
    name: string;
}

// merge the interface with the class
interface User extends IUser {}

class User {
    constructor(props: IUser) {
        Object.assign(this, props);
    }
}

It should be shorted to something like this, with just a little bit of syntactic sugar:

interface IUser {
    id: number;
    name: string;
}

class User uses IUser {
    constructor(props: IUser) {
        Object.assign(this, props);
    }
}

Where uses could be a new word, or extends / implements could work this way.

This Object.assign still bothers me a bit, but I think it's possible to get used to it.


Just to remember people the problem it solves:

interface IUser {
    id: number;
    name: string;
}

class User implements IUser {
    id: number;
    name: string;
    constructor(props: IUser) {
        this.id = props.id;
        this.name = props.name;
    }
}

It requires the programmer basically writing 3 times the same intention. I really hate doing this. "Oh, you want to quickly add a new information to the class? You must write it 3 times".

Writing 2 variables is already a pain, and when I must write many, it makes me want to scratch my brain >:/


Edit / Add: My uses solution, that shortens

interface User extends IUser {}
class User {...}

into

class User uses IUser {...}

looks like a good idea imho, if isn't possible to make implements automatically adding the interface variables into the class, and leaves extends with its requirements of having a super().

"Now", it would only need to make Typescript to watch if all the required variables are initialized, as the already possible

interface User extends IUser {}
class User {

(or as I am calling, _uses_)
removes the checking of initializations. Also, would require that Typescript watches the Object.assign results.

one thing to remember is that interface is an internal typescript thing only and blindly doing Object.assign(this, props) is dangerous on the server and can fall down in the real world on the server/browser in weird and unexpected ways. consider:

interface IUser {
    readonly id: number
    readonly name: string
}

interface User extends IUser {}

class User {
    private admin = false;

    constructor(props: IUser) {
        Object.assign(this, props)
    }
}

compiles to this js:

"use strict";
class User {
    constructor(props) {
        this.admin = false;
        Object.assign(this, props);
    }
}

and then let's say on the server you're doing:

const someUserInput = { id: 'user_123', name: 'bob', admin: true } // <== er337 hax0ring
const user = new User(someUserInput)
console.log('admin should be false, but: ', user.admin);

you could probably even do something like new User({ constructor: { prototype: {} } }) and replace the entire prototype maybe?

tl;dr object.assign is a poor workaround unless you're also validating/filtering input but at that point you might as well _type things three times_

Edit: throw a new option into the ring. what about adding support for public/protected/private inside the constructor?

class User {
  private admin = false;

  constructor(data: Partial<User>) {
    public id: string = data.id;
    public name: string = data.name
    protected featureFlag = data.featureFlag ?? false;
  }
}

@cmawhorter you're absolutelly right. Object.assign(this, any) is extremely dangerous if you're not validating input.
I'm my case, I always validate input for extra props, even in projects that I don't need this workaround.

class User {
  private admin = false;

  constructor(data: Partial<User>) {
    public id: string = data.id;
    public name: string = data.name
    protected featureFlag = data.featureFlag ?? false;
  }
}

@cmawhorter

This is really good and really could be implemented. I would improve with this:

class User {
  private admin = false;

  constructor(data: Partial<User>) {
    public id: string;
    public name: string;
    protected featureFlag = data.featureFlag ?? false;
  }
}

Without the initializer and with the Partial<$Class> parameter, it would implicitly mean that they would receive their value from the Partial argument.

Edit: The examples above have a problem easily fixable:

protected featureFlag = data.featureFlag ?? false;

What is the type of featureFlag? It's something like unknown (or any) & boolean. Cases like this must require the type to be explicitly declared.

How about this. Let's create new type keyword Props.

class User1 {

  public job: string = props1.job // normal property binding
  public name: string = props1.; // shorthand dot for props1.name binding
  public age: number = props1. * 10; // computed props binding

  constructor(props1: Props) {}

}

// In this way, typescript compiler can inference that type of Props in User1 class should conform below type
type PropsInUser1 = {
  job: number;
  name: string;
  age: number;
}


// usage
new User1({name: 'hello', age: 10, job: 'homeless'})

class User2Wrong {

  public name: string = props2.; // shorthand dot for props2.name binding
  public nickname: string = props2.nick // Compile Error. ts compiler can't infer field name 'nick' is included in props2

  constructor(props2: Props) {}

}

class User2Correct {

  public name: string = props2.; // shorthand for props2.name binding
  public nickname: string = 'call me :' + props2.nick // Now ts compiler can know field name 'nick' is included in props2 because of intersectioning of {nick:string}

  constructor(props2: Props & {nick: string}) {}

}

// type inference
type PropsInUser2Correct = {
  name: string;
}


// usage 
new User2Correct({name: 'hello', nick: 'hi'})



class User3 {

  public name: string;
  public age: number;

  constructor(props3: Props) {
     // ordinary prop assignment is still compatiable
    this.name = props3.name;
    this.age = props3.age;

    // Compile error. props3 only have name and age field.
    console.log(props3.message)
  }

}

class User4 {

  name: string = props4.;  // furthermore, we don't need to declare property modifier.
  age: number = props4. * 10; 

  constructor(props4: Props) {}

}
class User5 {

  name = props5.;  // type infererence works. typeof name : string.
  age = props5.; // In case of this, We can have 2 options. 
// 1: Compile error . ts compiler can not infer what type of age is. 
// 2. let's infer type of age as 'any'. sometimes, this loose typing will be useful.

  constructor(props5: Props & {name: string}) {}

}
interface IUser {
  name: string;
  age: number;
}

class User6 {

  name = props5.;  // intersectioning IUser with Props is very usefull in this case. 
  age = props5.; // type of name and age can be successfully inferenced from IUser

  // type intersection Props
  // Props have potential to have more property field
  constructor(props5: Props & IUser ) {}

  // generic Props
  // now, Props = IUser. field expansion of Props is prohibited.
  constructor(props5: Props<IUser> ) {}



  address: string = props5.; // in generic Props => Compile errror, type intersection Props => type inference works

}
class User7 {

  name: string = props7a.;  
  age: age = props7b.; 

  // multiple parameter works well.
  constructor(props7a: Props, props7b: Props ) {}

}

// type inference
type PropsAInUser7 {
  name: string;
}

// type inference
type PropsBInUser7 {
  age: number;
}

After I wrote this comment, I've found @rodrigolive 's idea is resemble to me. But somethings are different.

I don't like use props as this.props in initial field assignment, because using this in class field is not normal and may brought confuse to developers. Also, I don't like the construct keyword.

My Props keyword is required to be typed in constructor parameter. And only give hint to TS compiler, which is,
"Hey ts compiler, I'll pick some fields from this object parameter and assign it to class properties. You can inference type from the class property. Also, the field name will be equal with class property name. In some cases, I'll give you additional type info by using type intersectioning or generic typing. (See User6)"

Introducing a new keyword as common as Props would break about 95% of codebases that already use that at least once as a type Props or interface Props. I don't see the TS team being keen on introducing any new keywords for a feature that at best only reduces a small amount of boilerplate.

@mattdarveniza Code conflict can be resolved by using compilerOption. I can't agree with your last sentense. This is not small part of boilerplate code. When using named parameter, developer has to type 3 times repeatedly to bind ctor parameter to class property. Also, in almost cases, named parameter is more likely used than positioned parameter. New keyword has worth of it. If your words right, how TS team introduce Utility Types, like Omit, Partial, ReturnType

Something I've been playing around with: (Not in potential PR, just writing what I'd like to see)

interface IFooPublic {
  thin: string,
  large: number,
}

interface IFooPrivate {
  mine: any,
}

class Foo implements IFooPublic, IFooPrivate {
  constructor( public ...IFooPublic, private ...IFooPrivate ) {}
}

The idea is that you are destructuring the interface and adding it to the available properties of the class, while also providing the non-class interfaces for pushing data around. /shrug

let f = new Foo( {
  thin: "It's thin alright",
  large: 9001,
  mine: "minemineminemine",
})
Was this page helpful?
0 / 5 - 0 ratings

Related issues

manekinekko picture manekinekko  ·  3Comments

wmaurer picture wmaurer  ·  3Comments

blendsdk picture blendsdk  ·  3Comments

zhuravlikjb picture zhuravlikjb  ·  3Comments

MartynasZilinskas picture MartynasZilinskas  ·  3Comments