Home » c# » TypeScript and field initializers

TypeScript and field initializers

Posted by: admin November 30, 2017 Leave a comment

Questions:

How to init a new class in TS in such a way (example in C# to show what I want):

// ... some code before
return new MyClass { Field1 = "ASD", Field2 = "QWE" };
// ...  some code after

SOLUTION:
Classic JavaScript syntax:

return { Field1: "ASD", Field2: "QWE" };
Answers:

There is an issue on the TypeScript codeplex that describes this: Support for object initializers.

As stated, you can already do this by using interfaces in TypeScript instead of classes:

interface Name {
    first: string;
    last: string;
}
class Person {
    name: Name;
    age: number;
}

var bob: Person = {
    name: {
        first: "Bob",
        last: "Smith",
    },
    age: 35,
};

Questions:
Answers:

Updated 07/12/2016:
Typescript 2.1 introduces Mapped Types and provides Partial<T>, which allows you to do this….

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"}),
];

Original Answer:

My approach is to define a separate fields variable that you pass to the constructor. The trick is to redefine all the class fields for this initialiser as optional. When the object is created (with its defaults) you simply assign the initialiser object onto this;

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

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

or do it manually (bit more safe):

if (fields) {
    this.name = fields.name || this.name;       
    this.address = fields.address || this.address;        
    this.age = fields.age || this.age;        
}

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"
    }),
    new Person(new Person({name:"Joe"})) //shallow clone
]; 

and console output:

Person { name: 'default', address: 'default', age: 0 }
Person { name: 'Joe', address: 'default', age: 0 }
Person { name: 'Joe', address: 'planet Earth', age: 0 }
Person { name: 'Joe', address: 'planet Earth', age: 5 }
Person { name: 'Joe', address: 'default', age: 0 }   

This gives you basic safety and property initialization, but its all optional and can be out-of-order. You get the class’s defaults left alone if you don’t pass a field.

You can also mix it with required constructor parameters too — stick fields on the end.

About as close to C# style as you’re going to get I think (actual field-init syntax was rejected). I’d much prefer proper field initialiser, but doesn’t look like it will happen yet.

For comparison, If you use the casting approach, your initialiser object must have ALL the fields for the type you are casting to, plus don’t get any class specific functions (or derivations) created by the class itself.

Questions:
Answers:

In some scenarios it may be acceptable to use Object.create. The Mozilla reference includes a polyfill if you need back-compatibility or want to roll your own initializer function.

Applied to your example:

Object.create(Person.prototype, {
    'Field1': { value: 'ASD' },
    'Field2': { value: 'QWE' }
});

Useful Scenarios

  • Unit Tests
  • Inline declaration

In my case I found this useful in unit tests for two reasons:

  1. When testing expectations I often want to create a slim object as an expectation
  2. Unit test frameworks (like Jasmine) may compare the object prototype (__proto__) and fail the test. For example:
var actual = new MyClass();
actual.field1 = "ASD";
expect({ field1: "ASD" }).toEqual(actual); // fails

The output of the unit test failure will not yield a clue about what is mismatched.

  1. In unit tests I can be selective about what browsers I support

Finally, the solution proposed at http://typescript.codeplex.com/workitem/334 does not support inline json-style declaration. For example, the following does not compile:

var o = { 
  m: MyClass: { Field1:"ASD" }
};

Questions:
Answers:

I’d be more inclined to do it this way, using (optionally) automatic properties and defaults. You haven’t suggested that the two fields are part of a data structure, so that’s why I chose this way.

You could have the properties on the class and then assign them the usual way. And obviously they may or may not be required, so that’s something else too. It’s just that this is such nice syntactic sugar.

class MyClass{
    constructor(public Field1:string = "", public Field2:string = "")
    {
        // other constructor stuff
    }
}

var myClass = new MyClass("ASD", "QWE");
alert(myClass.Field1); // voila! statement completion on these properties

Questions:
Answers:

You can affect an anonymous object casted in your class type.
Bonus: In visual studio, you benefit of intellisense this way 🙂

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

Edit:

WARNING If the class has methods, the instance of your class will not get them.

This solution should be only used with interface.
For example, use this solution to store model as Plain Old Object.

interface IClass {
   Property1: string;
   Property2: string;
   PropertyBoolean: boolean;
   PropertyNumber: number;
}

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

Questions:
Answers:

I suggest an approach that does not require Typescript 2.1:

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

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

    public someFunc() {
        // todo
    }
}

let person = new Person(<Person>{ age:20, name:"John" });
person.someFunc();

key points:

  • Typescript 2.1 not required, Partial<T> not required
  • It supports functions (in comparison with simple type assertion which does not support functions)
Questions:
Answers:

The easiest way to do this is with type casting.

return <MyClass>{ Field1: "ASD", Field2: "QWE" };

Questions:
Answers:

Below is a solution that combines a shorter application of Object.assign to more closely model the original C# pattern.

But first, lets review the techniques offered so far, which include:

  1. Copy constructors that accept an object and apply that to Object.assign
  2. A clever Partial<T> trick within the copy constructor
  3. Use of “casting” against a POJO
  4. Leveraging Object.create instead of Object.assign

Of course, each have their pros/cons. Modifying a target class to create a copy constructor may not always be an option. And “casting” loses any functions associated with the target type. Object.create seems less appealing since it requires a rather verbose property descriptor map.

Shortest, General-Purpose Answer

So, here’s yet another approach that is somewhat simpler, maintains the type definition and associated function prototypes, and more closely models the intended C# pattern:

const john = Object.assign( new Person(), {
    name: "John",
    age: 29,
    address: "Earth"
});

That’s it. The only addition over the C# pattern is Object.assign along with 2 parenthesis and a comma. Check out the working example below to confirm it maintains the type’s function prototypes. No constructors required, and no clever tricks.

Working Example

This example shows how to initialize an object using an approximation of a C# field initializer:

class Person {
    name: string = '';
    address: string = '';
    age: number = 0;

    aboutMe() {
        return `Hi, I'm ${this.name}, aged ${this.age} and from ${this.address}`;
    }
}

// typescript field initializer (maintains "type" definition)
const john = Object.assign( new Person(), {
    name: "John",
    age: 29,
    address: "Earth"
});

// initialized object maintains aboutMe() function prototype
console.log( john.aboutMe() );