microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.03k stars 12.49k forks source link

[New Feature] Initialize Classes by Using an Object Initializer #3895

Closed mribichich closed 9 years ago

mribichich commented 9 years ago

Hi there, I come from a C# background and it has something great call Object Initializer. Which allows you to initialize an object inline, without specifying the object everytime.

C# docs: https://msdn.microsoft.com/en-us/library/bb397680.aspx

I would be cool if the compiler could do something like this:

new MyClass { Field1 = "ASD", Field2 = "QWE" };
RyanCavanaugh commented 9 years ago

C# needs this because it doesn't have object literals, but JavaScript doesn't have that problem. MyClass should be defining an appropriate constructor if this initialization pattern is common.

class MyClass {
  constructor(initializers: ...) { ... }
}

var x = new MyClass({field1: 'asd', 'field2: 'fgh' });
mribichich commented 9 years ago

Ok I understand that, but I'm not suggesting to do it for the same reason, but it would be cool for fast initialization.

In your example it makes you have a constructor and a mapping inside.

But what I'm suggesting, the compiler would do it for you.

kitsonk commented 9 years ago

What about for all the people who don't want you to break their object constructors? What solution do you propose for them?

OlegDokuka commented 9 years ago

If they want their object constructor, they should use it, but just imagine how it would be cool if compiller would help you to build the object like in C#. Even groovy support this feature.

It is easier way to initialize object with intellisense supporting where when you type the word you get some hint, if property exist, of course .(like in C#).

Today when we initialize interface we get some hint of property that exist in this interface, and, I think, everyone say "It is cool", and what happens when object can be installed with similar way?

Back to the @kitsonk answer. This feature just syntaxis shugar, and user that want to use object mapping in their constructor should choose the way that they want, and that is all.

Thanks!

DanielRosenwasser commented 9 years ago

I'll also point out that the production for this would necessitate that the open curly brace be on the same line due to ASI rules.

For instance:

new Foo
{
    bar = 10
}

The above is a new-expression followed by a block statement body.

CyrusNajmabadi commented 9 years ago

@DanielRosenwasser Is correct. But i would not consider this an ASI issue. The concern here would be syntactic ambiguity. What was unambiguously a specific construct in ES6 could now have two meanings if we introduced a production like this. If we did really want this, we'd likely need something very syntactically unambiguous to avoid these problems.

Also, see https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals This violates our goal of "Avoid[ing] adding expression-level syntax."

DanielRosenwasser commented 9 years ago

Yes, I don't consider there to be an issue with ASI, I consider this to be an easy pitfall for users.

RyanCavanaugh commented 9 years ago

Maybe we need a label for "This is a nice idea to bring to ESDiscuss" :wink:

OlegDokuka commented 9 years ago

wooooohoooo!)))

OlegDokuka commented 9 years ago

May be it is possible to add special rule to compiler. As I understand correctly, the main problem is that the JS ASI provide a cause for this syntax, but I point on that TS convert to JS and JS syntax is just a part of TS.

cervengoc commented 9 years ago

I would like to +1 this idea. When I have a simple class with many properties and some methods, etc., I would love to have the initializer syntax.

Some wrote that it's possible using interfaces. But now I use a class with methods in it, so I cannot just switch to interfaces. And I don't want to repeat myself by declaring a completely identical interface with all the members being optional.

Here I've read that the solution is to introduce a specific constructor, but that's also not a nice way, because that would use an "implicit interface" for parameter typing, and at the end I would still repeat myself.

some claims that it's only a syntactic sugar. But allowing for example template string for ES5 is a kind of syntactic sugar too IMO.

Also, constraining the opening brace to be at the same line doesn't sound like a very big deal.

TylerBrinkley commented 8 years ago

I know this is a closed issue, but it's something I'd very much like to see for my code gen situation where I cannot simply switch to using interfaces or add a new constructor.

dolanmiu commented 8 years ago

What about this?

http://stackoverflow.com/a/33752064/3481582

var anInstance: AClass = <AClass> {
    Property1: "Value",
    Property2: "Value",
    PropertyBoolean: true,
    PropertyNumber: 1
};
avonwyss commented 8 years ago

I know it is closed, but I ask to reconsider and I'm also +1 for this feature - because there are at least two cases where this is not just syntax sugar as far as I can tell.

Example with Function:

interface Foo {
    bar: any;
    (): void;
}

// how to create a Foo?
var foo: Foo;
foo = () => {}; // cannot convert type ... has non-optional property bar which is not present
foo.bar = "initialized!";

Example with Array:

interface Foo<T> extends Array<T> {
    bar: any;
}

// how to create a Foo?
var foo: Foo<any>;
foo = []; // cannot convert type ... has non-optional property bar which is not present
foo.bar = "initialized!";

Having object initializers like C# has would allow these to be handled in a typesafe manner without having to use an any cast and thus losing the check that all mandatory properties of these instances have been added/assigned a value.

TylerBrinkley commented 8 years ago

Sorry @dolanmiu, I didn't see your post until just now. The problem with that solution is that the constructor is never called. I'm using TypeScript to generate json as input to Json.NET and my constructor guarantees that important type information is serialized, eg. the $type field.

RyanCavanaugh commented 8 years ago

// how to create a Foo?

No need for any:

var foo: Foo;
foo = (() => {}) as Foo;
foo.bar = "initialized!";

You could do something like this, which ensures you pass all required properties:

function mix<T, U extends {[k: string]: {}}>(func: T, properties: U): T & U {
    Object.keys(properties).forEach(k => (func as any)[k] = properties[k]);
    return func as T & U;
}

var foo: Foo;
foo = mix(() => {}, { bar: 'initialized'});
avonwyss commented 8 years ago

Thanks Ryan for the suggestion. Whether any or Foo is used in the cast was not my point, but the fact remains that it does need a cast and after that the type safety is no longer warranted for (e.g. TS assumes that the properties are there, but they may be missing at runtime).

I'll try out the mix approach; reading the code here makes me realize that it should work thanks to duck typing but I'll have to see if it does actually do what I need.

MeirionHughes commented 8 years ago

here is my solution: http://stackoverflow.com/a/37682352/1657476

Just have a all-optional fields parameter on the constructor:

export class Person {
    public name: string;
    public address: string;
    public age: number;

    public constructor(
        fields?: {
            name?: string,
            address?: string,
            age?: number
        }) {
        if (fields) Object.assign(this, fields);
    }
}

usage:

let persons = [
    new Person(),
    new Person({name:"Joe"}),
    new Person({
        name:"Joe",
        address:"planet Earth"
    }),
    new Person({
        age:5,               
        address:"planet Earth",
        name:"Joe"
    })
]

I think its a simple and effective work-around.

grofit commented 8 years ago

I like it being more like the c# approach where you do not have to write any additional boilerplate, and for a lot of POJOs where you basically just want to pre populate some fields then add other stuff later via API callbacks etc you have the flexibility to do so.

Like for example if I was to do:

export class Person
{
   public name :string;
   public address: string;
   public age: number;
}

Then I wanted to populate various parts I could do:

// Populate name and age
var person = new Person {  name: "foo", age: 10 };

// Which is basically the same as
var person = new Person();
person.name = "foo";
person.age = 10;

However if you do have a custom constructor you can run that instead or as well as, basically I would just want to remove the common boilerplate where you end up having to have really verbose constructors full of optional params and the need to new up and then allocate the next N lines to myInstance.property = someValue;.

So making the c# style object initializer just act like a shorthand for manually applying the fields individually that alone would save time and yield benefits to developers.

aluanhaddad commented 8 years ago

@grofit how dies @MeirionHughes preclude that in any way? You're able to run any kind of pre or post initialization logic you would like. Also, it's worth noting that POJOs are generally synonymous with object literals and that is for a reason.

grofit commented 8 years ago

To do what @MeirionHughes does you need to write the following in every class which you want to use in this way:

// OTHER CLASS GOODNESS
    public constructor(
        fields?: {
        // one line per field with name and type
        }) {
        if (fields) Object.assign(this, fields);
    }
// OTHER CLASS GOODNESS

Also it will only work in ES5 due to the Object.assign, which is fine for most people but some of us have to also support ES3 in the older browsers.

However that ES5 thing to one side, given the more c# style approach it means I don't need to write any boilerplate constructors which is basically the EXACT SAME LINES as written above describing the class members, i.e:

export class MyClass
{
    public myType: string; // here is my field

    public constructor(
        fields?: {
           myType: string // just a duplication of the above field
        }) {
        if (fields) Object.assign(this, fields);
    }
}

The compiler should know what fields (and their types) are available within the class being instantiated so I do not need to do any of the above code. Why write code yourself to fulfill a task when the compiler could easily do it for you, meaning less code to maintain and more succinct models. In almost all cases typescripts value to developers is its removal of boilerplate code, and this is EXACTLY what this feature would achieve.

MeirionHughes commented 8 years ago

I have to agree with @grofit. Also there are plenty of other cases of typescript having generally unsupported functionality; es7 async/await is a good example; effectively must be transpiled down to at least es6.

Someone needs to propose new Person { name:"joe" } in es9/10/11 then we can pretend the syntax is valid js and can be used in ts; the proposition seems rather basic:

var person = new Person {name:"Joe"};

should be transpiled to:

var person = new Person();
person.name = "joe"; 

As I see it, the issue is not that you cannot do this; just that the work around requires extra boiler-plate code per-class implementation.

There are lots of examples of new features making life easier: i.e. You don't NEED async/await... you could do it manually.

MeirionHughes commented 8 years ago

I thought about this again and the main issue with it is when you have a lot of nesting going on; I guess the initialisation of the objects would have to be flattened down, or use functions to generate them inline.

ts:

return new [
   new Person() {
        name:"Joe"
   },
   new Person() {
        name:"James"
        info: new Info(){
            authority:"High"
        }
   },
]; 

js:

let person_1 = new Person();
person_1.name = "Joe";
let person_2 = new Person();
person_2.name = "James";
let info_1 = new Info();
info_1.authority = "High"
person_2.info = info_1;

let array_1 = [person_1, person_2];

return array_1;

not amazingly difficult.

aluanhaddad commented 8 years ago

If the type just has properties, it should not be a class. If it needs methods or super calls, use a factory.

avonwyss commented 8 years ago

@aluanhaddad Please see my post Feb 8th for situations where this is needed for type-safety. Using a factory does just shift the problem into the factory method; how to implement the factory in a type-safe manner?

@MeirionHughes I agree that code generation would be simple, but not the way you suggest it. Your approach could lead to a whole lot of variables and also problems in more complex cases (see below)... I would rather see code like this generated:

return new [
   (function() {
        var o = new Person();
        o.name="Joe";
        return o;
   })(),
   (function() {
        var o = new Person();
        o.name = "James";
        o.info = (function() {
            var o = new Info();
            o.authority = "High";
            return o;
        })();
        return o;
   })()
]; 

This approach would be straightforward to generate and because it remains a normal JS expression it would not break the semantics. To illustrate the point, take this example:

doSomething(Math.random() > 0.5
        ? new User() { name: "Peter" }
        : new Group() { members = [new User() { name: "John" }] }
    );

What code would your approach generate for this? It cannot know in advance which branch of the conditional statement will be used. The only way to solve this with a "flattening" approach is to generate explicit code for each conditional branch, which will result in up to 2^(number-of-conditionals) code branches, so that does not seem like a viable solution.

aluanhaddad commented 8 years ago

@avonwyss I see. I think a better alternative would be to have the compiler track mutations to the type across assignments. The problem with the Function and Array examples is that there is no way to express them declaratively in JavaScript and I fail to see how initializers would help in these cases.

avonwyss commented 8 years ago

@aluanhaddad My goal would primarily be to have type-safety when creating augmented functions and arrays, in contrast to objects which are created with the new where this would just be nice syntactic sugar for assigning several properties at once.

The compiler could and should tell me if I'm missing mandatory properties (especially when one starts using the nullability checks), however the necessity for a cast makes this plain impossible.

Syntax-wise, a new, array or function creation could be followed by an object literal which defines the properties and their values to be set on the result of the creation expression. Like so:

class Foo {
  bar: any;
}

const foo: Foo = new Foo() {
    bar: 0;
  };

type FnFoo {
  bar: any;
  (): void;
}

const fnFoo: FnFoo = () => {
    doSomething();
  } {
    bar: 10
  };

type ArrFoo<T> = Array<T> & {
  bar: any;
}

const arrFoo: ArrFoo<any> = [1, 2, 3] {
    bar: 20;
  };

Since the object creation is always also implying a call, wrapping the assignments into a function as shown in my previous comment should qualify as being a way to express that in JavaScript IMHO - if you don't think so you need to explain what you mean exactly

Also, with the introduction of readonly in TS2 (#12), it may be a good solution to allow read-only members to be assigned like this for functions and arrays since this can be seen as being part of the construction expression. As of now, it requires another cast to any to work... like this:

type ArrFoo<T> = Array<T> & {
    readonly bar: any;
}

const arrFoo: ArrFoo<any> = [1, 2, 3] as any;
(arrFoo as any).bar = 20;
aluanhaddad commented 8 years ago

@avonwyss actually I think that would be very valuable. I would like to avoid the new keyword if possible, but I think what you are proposing actually makes a lot of sense. Thanks for clarifying.

linvi commented 8 years ago

@RyanCavanaugh Your syntax is really not good, as it forces to have the field initializer within the constructor.

The goal of the C# syntax for field initialization is to have clear disctinction between the constructor and and how the fields are initalized.

The only way to do this currently with Typescript is to use

var user = new User();
user.name = 'nqdq';
user.email = 'dqkdq';
// ...

The C# sytax encapsulates the entire initialization within brackets that visually delimits the object initialization and therefore improve the code readability.

I personally think that this would be really a great improvement specially as Typescript does not support method/constructor overloading.

roman-petrov commented 8 years ago

+1 for this feature. It can save developers for writing a lot of boilerplate code, prettify code and improve program readability.

aluanhaddad commented 8 years ago

Honestly, the more TypeScript I write, the more I find myself wanting its features in other languages like C#. Like just the other day I was thinking "man I can't believe I can't say that this property is required and this one is optional".

Knaackee commented 7 years ago

What about the following.

No new feature is needed here. You can just initialize in the constructor.

Case 1: Every Property

export class Person {
    public id: number;
    public age: number;

    constructor(initializer?: Person) {
        // return if no initializer is passed in
        if (initializer === undefined) {
            return;
        }
        // apply 
        Object.keys(initializer).forEach((key, index) => {
            this[key] = initializer[key];
        });
    }
}

const myPerson = new Person({id: 2, age: 12});

Case 2: Just a subset of Properties

Instead of using Person as the initializer type, just use a interface or other object matching the properties.

export class Person {
    public id: number;
    public age: number;

    constructor(initializer?:{ age: number }) {
        // ... same as before ....
    }
}

const myPerson = new Person({id: 2, age: 12});

Instead of { age: number } you can use a interface or other object too.

Now, lets make it reusable

const initializeObject = <TTarget, TSource>(target: TTarget, source: TSource) => {
    if (target === undefined || source === undefined) {
        return;
    }
    Object.keys(source).forEach((key, index) => {
        target[key] = source[key];
    });
};

export class Person {
    public id: number;
    public age: number;

    constructor(initializer?: Person | { age: number}) {
        initializeObject(this, initializer);
    }
}
linvi commented 7 years ago

@Knaackee You do not seem to understand the difference between a constructor and an initializer.

The goal of the C# syntax for field initialization is to have clear distinction between the constructor and and how the fields are initalized.

In your example the constructor takes the responsibility of initializing all the fields. It means that if you want to build your object in 5 different ways, your ctor will be responsible to use 5 different logic to initialize the object, This is absurd.

Sometimes you want such responsibility to live outside of your constructor logic. This prevent the class code to be polluted by some code that it should not care about.

Knaackee commented 7 years ago

@linvi Iam aware of that. I just wanted to show a simple solution to set some properties in one line. I know that this is not the same as in c#.

dolanmiu commented 7 years ago

Initialisers are just syntactic sugar, you can have both a constructor AND initialiser code in C#

It just cleans up the code and makes it look nicer

It would be very nice to have this feature

Why has it taken this long for this feature to come through? Is this feature particularly difficult?

RyanCavanaugh commented 7 years ago

@dolanmiu this is tagged "Out of Scope" -- if you want it to happen, you should raise this with the ECMAScript committee as they control the syntax and semantics of class initialization

linvi commented 7 years ago

@RyanCavanaugh I do not understand. Typescript is not only reproducing ECMAScript. It is enhancing it. For example ECMAScript does not have interfaces or static typing, yet Typescript does.

Therefore why does this feature has to be out of scope? Specially when you see that people regularly request it for years.

dolanmiu commented 7 years ago

@RyanCavanaugh it is not supposed to be out of scope

Like I said above, object initialisation is only SYNTACTIC SUGAR

It does not affect the way ECMAScript does object construction at all. It is merely a shorthand notation. Which seems reasonable as a feature of Typescript.

Please read up on how C# does object initialisation

mhegazy commented 7 years ago

@dolanmiu, when considering new TS features, we follow the TypeScript desing goals. It is not about syntactic sugar or not, it is about goal 6. Align with current and future ECMAScript proposals..

The committee has reserved the syntax after : in declaration, and is guarantee not to step on it, so we have that are for adding syntactic features in the type declaration domain without conflicting with future versions of ECMAScript. For anything else, however, we do not have this guarantee. the committee could use this syntax in the future to mean something different; this would result in a breaking change for our customers, something we do like to avoid whenever possible.

For such proposals, we ask you present them to TC39 to avoid any potiential conflict with the ECMAScript standard.

MeirionHughes commented 7 years ago

Typescript 2.1 just made this a whole lot easier with the Mapped Types. You can now do:

class Person {
    public name: string = "default"
    public address: string = "default"
    public age: number = 0;

    public constructor(init?:Partial<Person>) {
        Object.assign(this, init);
    }
}

let persons = [
    new Person(),
    new Person({}),
    new Person({name:"John"}),
    new Person({address:"Earth"}),    
    new Person({age:20, address:"Earth", name:"John"}),
];
linvi commented 7 years ago

@MeirionHughes interesting, thanks for sharing.

OliverJAsh commented 7 years ago

@MeirionHughes Unfortunately this means you lose some type safety on properties without defaults:

{
    class Person {
        public age: number;

        public constructor(init?:Partial<Person>) {
            Object.assign(this, init);
        }
    }

    let persons = [
        new Person(),
        new Person({age:1}),
        new Person({}), // want this to error!
    ];
}

What we actually want is NamedProps = AllProps - PropsWithoutDefaults. I'm not sure mapped types have that flexibility?

MeirionHughes commented 7 years ago

yeah this was raised on my SO answer too; I think the only current sane solution there is to define any class member with defaults as optional. i.e.

class Person {
  public name?: string = "default";
  public age: number;

  public constructor(init: Person) {
    Object.assign(this, init);
  }
}

let people = [
  new Person(), //error
  new Person({}), //error
  new Person({ age: 10 }), //ok 
  new Person({ age: 10, name: "John" }), //ok
];

pro:

con:

of course an argument could be made that if the fields are required to make an instance, then they should be true parameters of the constructor. i.e. constructor(age: number, others: Partial<Person>)

OliverJAsh commented 7 years ago

Another con there is name would be typed as string | undefined.

I think this really needs subtraction types, #4183

rappinc commented 7 years ago

When compiling this error appears: Cannot find name 'Partial'. typescript version: 2.1.5

wijnsema commented 7 years ago

Eh, what about

class Person {
    public name: string
    public age: number
}

var p: Person = { age: 23, name: 'Ronald' }

Type safe, intellisense

cervengoc commented 7 years ago

@wijnsema Yes, type safe, inellisense, but not a Person instance actually, which is quite a drawback :) So for example your constructor will never run.

And note also, that to make this type-compatibility work, you have to define every member of the class, in your object literal, including methods for example.

iddan commented 7 years ago

Consider this syntax:

class Person {
    constructor({ name });
}

instead of

class Person {
    constructor({ name }) {
        Object.assign(this, { name });
    }
}
aluanhaddad commented 7 years ago

Consider this syntax:

class Person {
constructor({ name });
}

instead of

class Person {
constructor({ name }) {
Object.assign(this, { name });
}
}

That seems reasonable except that type annotations on destructured parameters are already extremely unpleasant to write and it is good for parameters to have type annotations so it doesn't buy you much.

Also by omitting

Object.assign(this, { name });

you are missing out on the opportunity to write

Object.feeze(Object.assign(this, { name }));
iddan commented 7 years ago

@aluanhaddad I do, but I always can go back to normal constructor. The thing is that you won't need type annotations on the destructor as they can be inferred by the class's annotations