TypeScript and field initializers
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.
Update
Since writing this answer, better ways have come up. Please see the other answers below that have more votes and a better answer. I cannot remove this answer since it's marked as accepted.
Old answer
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 {
givenName: string;
surname: string;
}
class Person {
name: Name;
age: number;
}
var bob: Person = {
name: {
givenName: "Bob",
surname: "Smith",
},
age: 35,
};
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:
- Copy constructors that accept an object and apply that to
Object.assign
- A clever
Partial<T>
trick within the copy constructor - Use of "casting" against a POJO
- Leveraging
Object.create
instead ofObject.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() );