Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript Object Initializer Syntax #16737

Closed
MrMatthewLayton opened this issue Jun 26, 2017 · 14 comments
Closed

TypeScript Object Initializer Syntax #16737

MrMatthewLayton opened this issue Jun 26, 2017 · 14 comments
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript

Comments

@MrMatthewLayton
Copy link

Introduction

I would like to submit this proposal for discussion (and hopefully implementation into the language spec) to introduce object initializer syntax into TypeScript.

Rationale

TypeScript already gives us the ability to create record type classes through syntactic sugar that allows us to declare member variables that can be assigned via the constructor.

class Point {
    constructor(
        public x: number = 0,
        public y: number = 0) {
    }
}

This is really useful, and I find myself using this syntax more often than not, but there are cases where this syntax is not appropriate for an object; granted the example above essentially exposes a default constructor, since all parameters are optional, one could create a point with no arguments.

const point = new Point();

There are however cases where this is not appropriate; we may in some cases want to implement classes where member variables or properties are not set during constructor initialization. Point is actually not a great candidate for this but for argument sake...

class Point {
    public x: number = 0;
    public y: number = 0;

    constructor () {
    }
}

If we wanted to construct a point implemented like the one above, we would currently have to do so like this.

const point = new Point();
point.x = 123;
point.y = 456;

Solution

It would be nice if we had object initializer syntax which would make this a little easier.

const point = new Point() {
    x = 123;
    y = 456;
}

Which would compile to

const point = (function() {
    const point = new Point();
    point.x = 123;
    point.y = 456;
    return point;
})();

Before I Forget

I've read a few articles on alternative ways to do this with TypeScript, but frankly I don't like them, mainly because most of them only specify contractual constraints on object declarations and initialization, but require object literals to do so; my solution ensures that actually, you get back the expected type.

interface IPoint {
    x?: number;
    y?: number;
}

const point: IPoint = {
    x = 123;
}

This would compile, but it doesn't actually give you a Point instance.

@kitsonk
Copy link
Contributor

kitsonk commented Jun 26, 2017

This feels like a dupe of #3895 (which is marked as out of scope).

If we wanted to construct a point implemented like the one above, we would currently have to do so like this.

const point = new Point();
point.x = 123;
point.y = 456;

Or you could do this:

class Point {
  constructor (public x: number = 0, public y: number = 0) {}
}

const point = new Point(1, 2);

but frankly I don't like them

For all things like this, you really have to have a compelling reason why you would want to break compatibility with ECMAScript/JavaScript. Developer convenience doesn't seem like one of those.

@ghost
Copy link

ghost commented Jun 26, 2017

const point = new Point() {
    x = 123;
    y = 456;
}

This isn't any more readable than:

const point = new Point({
    x: 123,
    y: 456,
});

That can be accomplished with:

class Point {
    readonly x: number;
    readonly y: number;
    constructor(options: { x: number, y: number }) {
        this.x = options.x;
        this.y = options.y;
    }
}

The advantage of this is that you can make your fields readonly, while with the syntax you propose it seems like they would have to be mutable, even if only intended to be set during initialization. It also gives you a place to do assertions, and lets the initialization options differ from the internal structure of the fields.

@kitsonk
Copy link
Contributor

kitsonk commented Jun 26, 2017

Or an even more complete re-implementation which includes the default values:

class Point {
    readonly x: number;
    readonly y: number;
    constructor({ x = 0, y = 0 }: { x?: number, y?: number }) {
        this.x = x;
        this.y = y;
    }
}

const p1 = new Point({ x: 123 });
const p2 = new Point({ y: 256 });

@ghost ghost added the Out of Scope This idea sits outside of the TypeScript language design constraints label Jun 26, 2017
@RyanCavanaugh RyanCavanaugh added the Suggestion An idea for TypeScript label Jun 26, 2017
@mhegazy mhegazy closed this as completed Nov 20, 2017
@Cifram
Copy link

Cifram commented Feb 18, 2018

I know this has been closed, but I would request it be re-opened, as there is a compelling reason for this feature which wasn't covered in this thread.

The constructor shorthand, like:

class Point {
    constructor(public x: number, public y: number)
}

is great, in that it avoids a lot of boilerplate, and provides a completeness guarantee. That is, it's guaranteed that every field is explicitly initialized. However, it has a major downside, in that it becomes unwieldy if your class has more than a couple fields. Image this class instead:

class User {
    constructor(
        public name: string,
        public birthday: Date,
        public regdate: Date,
        public social: string,
        public passwordHash: string,
        public isAdmin: bool
    )
}

let myUser = new User(name, regdate, birthday, ssn, hash, false)

I need to break it into multiple lines because it's getting too long! And this is still fairly mild. In real production systems, there are plenty of objects that can have twice that many fields. In order to call this constructor, I need to provide six ordered, unlabeled arguments, some of which are of the same type. In fact, notice the bug here? The regdate and birthday are backwards, which could cause all sorts of subtle bugs.

There was an alternate example provided above which uses a single constructor argument which takes an object with a specified structure. Which would look like this:

class User {
    public name: string,
    public birthday: Date,
    public regdate: Date,
    public social: string,
    public passwordHash: string,
    public isAdmin: bool
    constructor(args: {
        name: string,
        birthday: Date,
        regdate: Date,
        social: string,
        passwordHash: string,
        isAdmin: bool
    }) {
        self.name = args.name
        self.birthday = args.birthday
        self.regdate = args.regdate
        self.social = args.social
        self.passwordHash = args.passwordHash
        self.isAdmin = args.isAdmin
    }
}

let myUser = new User({
    name = name,
    birthday = birthday,
    regdate = regdate,
    social = social,
    passwordHash = passwordHash,
    isAdmin = false
})

The completeness guarantee is still here, which is great, and the initialization is now labeled instead of ordered so it'll be much harder to accidentally mix up arguments of the same type. But the boilerplate is ridiculous!

With a generic object initializer syntax, we can get the best of all worlds: no boilerplate, completeness guaranteed, and labeled instead of ordered initialization. This example now looks like this:

class User {
    public name: string,
    public birthday: Date,
    public regdate: Date,
    public social: string,
    public passwordHash: string,
    public isAdmin: bool
}

let myUser = new Class() {
    name = name,
    birthday = birthday,
    regdate = regdate,
    social = ssn,
    passwordHash = passwordHash,
    isAdmin = false
}

So much better!

(Note: All of this is assuming this feature comes with a completeness guarantee, i.e. the object initializer must contain every field that's in the object it's initializing, which doesn't have a default value. Without that, this feature has a lot less value. But I see no reason why you wouldn't force completeness on object initializers.)

@ghost
Copy link

ghost commented Feb 20, 2018

@Cifram This pattern would work without duplicating the properties:

function init<T>(a: T, b: T): void { Object.assign(a, b) }

interface UserData {
    name: string;
    birthday: Date;
    regdate: Date;
    social: string;
    passwordHash: string;
    isAdmin: boolean;
}
interface User extends UserData {}
class User {
    constructor(data: UserData) {
        init<UserData>(this, data);
    }

    deservesCake() {
        return this.birthday.getDate() == new Date().getDate() || this.isAdmin;
    }
}

Of course, if you don't need to define any methods you could just use an interface directly:

interface User { ... }
const myUser: User = { ... };

@Cifram
Copy link

Cifram commented Feb 20, 2018

That does largely solve the problem, though I think the init function is a little unfortunate.

@murraybauer
Copy link

@andy-ms @Cifram Except if you need to define decorators on your class properties!!

@Ruffo324
Copy link

This isn't any more readable than:

const point = new Point({
    x: 123,
    y: 456,
}); 

That can be accomplished with:

class Point {
    readonly x: number;
    readonly y: number;
    constructor(options: { x: number, y: number }) {
        this.x = options.x;
        this.y = options.y;
    }
} 

Or with less typing writing:

class Point {
    readonly x: number;
    readonly y: number;
    constructor(point: Point) {
        this.x = point.x;
        this.y = point.y;
    }
}

Or with much more less writing, but (i think, not tested) slower:
(I mean less writing relative to the number of properties)

class Point {
    readonly x: number;
    readonly y: number;
    constructor(point: Point) {
        Object.keys(point).forEach((key) => this[key] = point[key]);
    }
}

@MathieuLarocque
Copy link

MathieuLarocque commented Jun 19, 2018

Here is the cleanest version I can think of:

class Greeter {
    greeting: string = this.input.greeting || 'default';
    greeting2?: string = this.input.greeting2;
    constructor(private input: Partial<Greeter> = {}) {}
    greet(): string {
        return this.greeting;
    }
    greet2(): string {
        return this.greeting2 || 'hi';
    }
}
let greeter = new Greeter({ greeting: 'hello' });
let greeterNoInput = new Greeter();

The only problem with this is that because the input is a Partial of itself, all the fields are optional. That means you have to handle the default values correctly or make the properties themselves optional. At least this way avoids Object.assign or Object.keys which isn't very Typescript friendly.
Hope this helps.

@gerardcarbo
Copy link

Not very elegant but preverves default values:

class Point {
  x ?= 0;
  y ?= 0;

  constructor(p: Point) {
      Object.assign(this, p);
  }
}

let p1 = new Point({x: 1}); --> (1,0)
let p2 = new Point({y: 1}); --> (0,1)

@Elfayer
Copy link

Elfayer commented Jun 26, 2019

When I receive data from my API, it is a JS object, not TypeScript yet. I wish I could do as:

const point = new Point(...data)

where data equals { x: 1, y: 2 }
Each key would match its proper attribute name.

@kitsonk
Copy link
Contributor

kitsonk commented Jun 26, 2019

@Elfayer TypeScript is a structural typing system, because JavaScript is largely structural. You don't need to apply a nominal constructor to some arbitrary data to have it be that type. If it is structurally the same, it is that type in TypeScript, irrespective of how it was created. Don't try to fit a square peg in a round hole. It will lead to 😭 .

@Elfayer
Copy link

Elfayer commented Jun 26, 2019

I'm not sure what you mean. I just wish we could have keyword parameters: new Point(x=2, y=3)
And allow such syntax to exist for very long constructors: new Point(...obj)
This is available in Python for example: https://docs.python.org/dev/whatsnew/3.5.html#pep-448-additional-unpacking-generalizations

>>> print(*[1], *[2], 3, *[4, 5])
1 2 3 4 5

>>> def fn(a, b, c, d):
...     print(a, b, c, d)
...

>>> fn(**{'a': 1, 'c': 3}, **{'b': 2, 'd': 4})
1 2 3 4

I'm not a Python developer, but I like the flexibility it brings to the language.

@kitsonk
Copy link
Contributor

kitsonk commented Jun 26, 2019

Because you don't understand what I mean is why you are asking for something that doesn't make sense in TypeScript.

You example again:

const data = { x: 1, y: 1};

interface Point {
    x: number;
    y: number;
}

const point: Point = { ...data };

TypeScript is a structural typing system. If you wanted a new way to create instances of a class though, you would be best to advocate for this in ECMAScript/JavaScript (https://esdiscuss.org/) and if it got adopted there, TypeScript would follow along. TypeScript is a (mostly) an erasable type system, not the languages syntax.

@microsoft microsoft locked as resolved and limited conversation to collaborators Jun 26, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

10 participants