-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Comments
This feels like a dupe of #3895 (which is marked as out of scope).
Or you could do this: class Point {
constructor (public x: number = 0, public y: number = 0) {}
}
const point = new Point(1, 2);
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. |
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. |
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 }); |
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.) |
@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 = { ... }; |
That does largely solve the problem, though I think the |
@andy-ms @Cifram Except if you need to define decorators on your class properties!! |
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: class Point {
readonly x: number;
readonly y: number;
constructor(point: Point) {
Object.keys(point).forEach((key) => this[key] = point[key]);
}
} |
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 |
Not very elegant but preverves default values:
|
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 |
@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 😭 . |
I'm not sure what you mean. I just wish we could have keyword parameters: >>> 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. |
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. |
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.
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.
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...If we wanted to construct a point implemented like the one above, we would currently have to do so like this.
Solution
It would be nice if we had object initializer syntax which would make this a little easier.
Which would compile to
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.
This would compile, but it doesn't actually give you a
Point
instance.The text was updated successfully, but these errors were encountered: