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

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

Closed
mribichich opened this issue Jul 16, 2015 · 87 comments
Closed

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

mribichich opened this issue Jul 16, 2015 · 87 comments
Labels
Out of Scope This idea sits outside of the TypeScript language design constraints Suggestion An idea for TypeScript

Comments

@mribichich
Copy link

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 RyanCavanaugh added Suggestion An idea for TypeScript Out of Scope This idea sits outside of the TypeScript language design constraints labels Jul 16, 2015
@RyanCavanaugh
Copy link
Member

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
Copy link
Author

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
Copy link
Contributor

kitsonk commented Jul 20, 2015

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

@OlegDokuka
Copy link

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
Copy link
Member

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
Copy link
Contributor

@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
Copy link
Member

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

@RyanCavanaugh
Copy link
Member

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

@OlegDokuka
Copy link

wooooohoooo!)))

@OlegDokuka
Copy link

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
Copy link

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
Copy link

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
Copy link

What about this?

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

var anInstance: AClass = <AClass> {
    Property1: "Value",
    Property2: "Value",
    PropertyBoolean: true,
    PropertyNumber: 1
};

@avonwyss
Copy link

avonwyss commented Feb 8, 2016

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
Copy link

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
Copy link
Member

// 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
Copy link

avonwyss commented Feb 11, 2016

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
Copy link

MeirionHughes commented Jun 8, 2016

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
Copy link

grofit commented Jun 15, 2016

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
Copy link
Contributor

@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
Copy link

grofit commented Jun 15, 2016

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
Copy link

MeirionHughes commented Jun 15, 2016

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
Copy link

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
Copy link
Contributor

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

@avonwyss
Copy link

avonwyss commented Sep 17, 2016

@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
Copy link
Contributor

@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
Copy link

@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;

@aj0strow
Copy link

Immutable JS Record Replacement:

class Person {
    readonly name: string;
    readonly age: number;

    constructor(...initData: Partial<Person>[]) {
        Object.assign(this, initPerson, ...initData)
    }
}

const initPerson: Partial<Person> = {
    name: "",
    age: 0,
}

const v1 = new Person()
const v2 = new Person(v1, { name: "AJ", age: 23 })
const v3 = new Person(v2, { age: 24 })

@rihei
Copy link

rihei commented Nov 8, 2017

@FrogTheFrog, could you explain what is the difference in using ObjectFields vs. just the type itself:

export class EmailOptions {
    constructor(fields: EmailOptions) {
       Object.assign(this, fields);
    }
    replyTo: string;
    fromEmail?: string;
    fromName?: string;
    recipients?: ContentVariable[];
    templateName?: string;
}

@MeirionHughes
Copy link

@rihei I don't see there being a difference either. There is scope to pull the object fields and, specifically, remove functions. but you can't do that until type operators/subtraction drops.

@FrogTheFrog
Copy link

@rihei Honestly, I don't know... At that time I must have been searching for something that "worked". Then I found some new way to write it, got overexcited and posted here. Only to find out that it's more of the same.

@rihei
Copy link

rihei commented Nov 8, 2017

Ok, thank you both! :) I’m new to all this and wasn’t sure I got it right. As a C# programmer I was surprised not to have an object initialization syntax in TS. However, it seems to be a pretty good option to write this kind of one-line constructor to DTO classes etc.

@aluanhaddad
Copy link
Contributor

aluanhaddad commented Nov 8, 2017

@rihei bear in mind that classes in TypeScript do not play by any means the same that they play in C#. Thinking otherwise is a road to pain and sadness.

I recommend that you use objects for DTOs instead.

@rihei
Copy link

rihei commented Nov 10, 2017

@aluanhaddad, by objects you mean anonymous objects? In most cases I prefer using strongly typed DTOs to make sure they have correct fields on every call site. But this is off-topic here :) I vote for the actual object initialization syntax, but am ok with the current one-line constructor, too.

@aluanhaddad
Copy link
Contributor

I am talking about object literals. Object literals are strongly typed.

@kitsonk
Copy link
Contributor

kitsonk commented Nov 11, 2017

In JavaScript anonymous objects is not a term commonly used, usually ported from people familiar with other languages... Objects don't have names in JavaScript, unlike Functions, which can have names and thereby anonymous functions are a thing. Some people apply the anonymous objects to object literals (e.g. objects created with curly braces, versus a constructor function/class). And as @aluanhaddad says those are implicitly strongly typed at creation, and their assignability differs slightly in TypeScript because the objects are considered fresh and so they are checked for excess properties at assignment. The following two lines produce exactly the same output (and should have the same runtime footprint):

new Object();
{};

On the other hand, these are different:

Object.create({});
{};

In that the first, will have a prototype strictly equal to the object literal that is passed in as the create argument, where as the second will only have a prototype strictly equal to Object.prototype.

I still think people are projecting from other languages here... There are reasons why the language doesn't account for some of these features, because the language essentially has them, just in slightly different syntaxes and constructs.

@aluanhaddad
Copy link
Contributor

@kitsonk well stated. I would say that is the correct way to look at it.

I particularly get concerned when people discuss DTO classes because that implies that they plan to put in place an entire layer for serialization features that are already here in the language.

To talk about another language briefly @rihei mentioned C# and, while it is common to serialize and deserialize objects to specific DTO types in that language, it hasn't been necessary to do that in C# since ~2008.

@nesbocaj
Copy link

nesbocaj commented Mar 22, 2018

Sorry for reviving such an old issue, but for anyone still finding the lack of this feature to be an inconvinience, there's this method:

const obj = { ...new MyClass(), field1 = "ASD", field2 = "QWE" };

Of course it would be nice if the prototype was preserved, but I guess we can't have everything...

Edit:

For instances where the prototype must be preserved, one could instead do this:

function initializeObject(obj, properties) {
  return Object.create(
    Object.getPrototypeOf(obj),
    Object.getOwnPropertyDescriptors({ ...obj, ...properties }))
}

const obj = initializeObject(new MyClass(), { field1 = "ASD", field2 = "QWE" });

@vivatum
Copy link

vivatum commented Jun 14, 2018

Base on TypeScript docs...

export class User {
  id: number;
  name: string;
  email: string;

  constructor(id: number, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
  }
}

let newUser = new User(123, "John", "john@mail.com");

@DKroot
Copy link

DKroot commented Jun 14, 2018

@vivatum Of course, a class can have (multiple) constructors which can be used just like in other OO languages. However, (C#) object initializers are quite powerful syntactic sugar because they enable the ability to initialize an object using arbitrary, ad-hoc combinations of fields. They are good for 2 reasons:

  1. Handy for classes with many fields and initialization needs.
  2. Cover what (vast) majority of constructors do: taking arguments and assigning them to fields thus eliminating the need in writing many constructors

Can you live without object initializers? Of course. Most OO languages do.
Are they a good idea to support? I think so.

@manoharreddyporeddy
Copy link

What about this?

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

var anInstance: AClass = <AClass> {
    Property1: "Value",
    Property2: "Value",
    PropertyBoolean: true,
    PropertyNumber: 1
};

In fact, this is what is working in 2018

@FrogTheFrog
Copy link

FrogTheFrog commented Nov 26, 2018

@manoharreddyporeddy That's a BIG "no no". Your class will not have correct prototype, methods and etc. since you're casting (not constructing) a normal object to type AClass.

@manoharreddyporeddy
Copy link

@FrogTheFrog
@https://github.com/FrogTheFrog

I am actually running unit tests.
They need input, i have a json object, the casting works fine.

  1. I actually have nested and a long structure.
    What is the best way to do, any example?

  2. I am not sure why this exists in first place, when it works perfectly fine.

@avonwyss
Copy link

@manoharreddyporeddy If you have working unit tests then they are simply not checking anything related to what @FrogTheFrog has written. Since the type system of TypeScript is a compile-time thing you will not get any casting error at runtime as you would in Java or C#. This does not change the fact, however, that the prototype chain is not correctly set up, and that you force the compiler to assume that the object has the correct prototype but this will not be true at runtime. This is JS fundamentals.

@MeirionHughes
Copy link

MeirionHughes commented Nov 27, 2018

Perhaps the way to move forward with this proposal is to come at it from the existing constructor field syntax: i.e.

class Foo {
  constructor(public myName) { } 
}

gets extended to support nesting of public/private.

class Foo {
  constructor(opts: {public myName} ) { } 
}

or some other keyword that explicitly flattens the passed type:

class Foo {
  constructor(flatten public opts: {myName} ) { } 
}

both resulting in the same class Foo with a field myName

cc: @RyanCavanaugh

@avonwyss
Copy link

@MeirionHughes The constructor syntax does not address all cases where an object initializer seems to be the only way to avoid unsafe casts as noted earlier in the discussion. An object initializer syntax without support for augmented functions and arrays would be pointless IMHO.

@manoharreddyporeddy
Copy link

@FrogTheFrog @avonwyss Others

Thanks, I am not an expert in this but is similar structure is below, and it has no functions (Good point you mentioned) in the Cls1

Everything as a class is good in Typescript.

Example: Below discussion is on what is the best way to do.

class Cls1 {
public key1: string;
public key2: number;
public key3: string;
// no functions - just the object assignment
}

Option 1: ---- I have a complex structure, this is unit testing, this looks very simple to use, I can copy paste the network response from the browser, when several unit tests with different values, then this will fare much easier ----
let obj1: Cls1 = {
key1: 'val1',
key2: 0,
key3: 'val3'
}

Option 2 ---- default constrcutor, and assignments ----
let obj1: Cls1 = new Cls1();
obj1.key1 = 'val1';
obj1.key2 = 0;
obj1.key3 = 'val3';

Option 3 ---- paramterised constructor is needs to be created, inside it assignments ----
let obj1: Cls1 = new Cls1('val1', 0, 'val3');

Option 4
< Any other option you may have, easy for complex structure, and easy to copy paste from JSON object will be better, similar to Option 1>

What is the best option for unit testing?
What is the best option going prod?

Thank you

@dolanmiu
Copy link

@manoharreddyporeddy

I have written an answer above this chain, but the feedback is very mixed, so I re-thought about it.

After using TypeScript for a while, I think doing this is best

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

public class Person {
    constructor(private readonly options: MyOptions)
}

then you can do this:

new Person({
    name: "Tom",
    age: 30
})

Advantage of doing it like this is that you are exposing less variables out. It's less leaky

Doing it the classical way like below is bad because now those variables are exposed and can be changed anywhere else in the app:

const klass = new Person();
klass.name = "Tom";
klass.age = 30;

There's nothing stopping you of modifying klass.name anywhere. You could say "oh, but I want it to be changed!". In which I will say, use a setter rather than expose what should be private.

Not only that, but it is less robust. it means its possible to instantiate a Person without a name or age.

What I am trying to say is, its separating the stuff which really should be in the constructor.

@ghost
Copy link

ghost commented Nov 30, 2018

I am not sure, if it has changed in newer typescript versions - mine is 3.1.6 -, but this does not compile (regardless if strict or not compiler options):

class MyClass implements MyInterface {
    a: string; 
    b: number;

    constructor(i: MyInterface) {
        Object.assign(this, i);
    }
}

interface MyInterface {
    a: string;
    b: number;
}

Error: [ts] Property 'a' has no initializer and is not definitely assigned in the constructor. [2564]
Same for b.

Ideally, I would like to simply write following to let a new class object initialize from other type (like object initializer):

class MyClass implements MyInterface {

    constructor(i: MyInterface) {
        Object.assign(this, i);
    }
}

interface MyInterface {
    a: string;
    b: number;
}

Current error: Class 'MyClass' incorrectly implements interface 'MyInterface'. Property 'a' is missing in type 'MyClass'. [2420]

Did I miss some workaround mentioned above / are the plans to make class constructors more type aware?

@avonwyss
Copy link

avonwyss commented Nov 30, 2018

@Ford04 This happens when both strictPropertyInitialization and strictNullChecks is enabled, see Playground (TS 3.2.1-insiders.20181128). I guess when using strictPropertyInitialization you should manually copy these for now.

@ghost
Copy link

ghost commented Nov 30, 2018

thanks, didn't know about that strictPropertyInitialization option.
My current workaround is to use the bang operator like this

class MyClass implements MyInterface {
    a!: string; 
    b!: number;
    constructor(i: MyInterface) { Object.assign(this, i); }

, as I find these strict compiler checks very helpful.

@Gianthra
Copy link

I don't know if this helps, however I've been doing this for my items, it's not the same but it seems to match what we're tying to do here and it follows the typescript syntax style. (I'm on TS 3.1.1 I think)

let car:Vehicle = {
    Brand: "Tesla",
    // gives compiler error for missing values
    // and for child values that are missing
    //Type: "Electric"
};

@arturoarevalo
Copy link

Coming from a C# background I find object initializers very useful. For a project I'm working on I reached the following solution.

First, define a generic base class (I called it DTO) that will take care of initialization of the properties it receives as an object in the constructor. This class will never be instantiated directly, so I made it abstract. The constructor receives a single parameter, whose type is derived from the generic class EXCLUDING methods.

type ExcludeMethods<T> =
    Pick<T, { [K in keyof T]: T[K] extends (_: any) => any ? never : K }[keyof T]>;
    
abstract class DTO<T> {
    public constructor(initializer: ExcludeMethods<T>) {
        Object.assign(this, initializer);
    }
}

Now, we can define our "initializable" classes extending it.

class Person extends DTO<Person> {

    public readonly id: string;
    public readonly value1: number;
    public readonly value2: number;

}

const person = new Person({
    id: "id",
    value1: 10,
    value2: 20
});

Type validation and refactoring field names work like a charm. Not as clean as a C# object initializer, but gets its job done

@feluxe
Copy link

feluxe commented Jun 27, 2019

@arturoarevalo This is brilliant! Structs for TypeScript. I find the class/constructor syntax way to verbose and interfaces suck when you need to type check on them. This seems like a nice workaround. Thanks for posting this.

Edit: There is also this: https://github.com/alexeyraspopov/dataclass which looks very similar but allows default values and more.

@IonelLupu
Copy link

@arturoarevalo It is possible to have constructor initializer in the parent of a class? For example:

abstract class Person{
    someField: string;
    constructor(initializer: SomeMagicHere){
        Object.assign(this, initializer);
    }
}

class Admin extends Person{
    name: string
}

var john =  new Admin({name: 'John'}); // I need to get only the Admin's fields and not also the Person's fields 

To fix this I've added an initializer method under Person:

export type ChildObject<Child, Parent> = Partial<Omit<Child, keyof Parent>>;
abstract class Person{
    fill(data: ChildObject<this, Person>){ ... } 
}

var john =  (new Admin).fill({role: 'John'}); // proper intellisense

But I don't like this workaround. And setting it in the constructor: constructor(data: ChildObject<this, Entity>) {} gives: A 'this' type is available only in a non-static member of a class or interface.

For the best DX I need the initializer to go into the constructor

@microsoft microsoft locked as resolved and limited conversation to collaborators Sep 17, 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