Lets say I have a TypeScript class with a property called birthDate
and 3 getters based on this property: year
, month
and day
.
class User {
birthDate: Date;
get month(): number {
return this.birthDate.getMonth();
}
get year(): number {
return this.birthDate.getFullYear();
}
get day(): number {
return this.birthDate.getDay();
}
}
I can instantiate an User
, set a birthDate
and use the getters normally.
const user = new User();
user.birthDate = new Date();
console.log(user.year, user.month, user.day);
But, obviously, I can't set a year directly because I have only a getter (and not a setter)
// this does not work as expected
user.year = 2022
On the other hand, when I try to define an User
with a plain object the TypeScript compiler generates an error saying that I have missing properties (year
, month
and day
):
// this does not works... why?
const user: User = {
birthDate: new Date()
}
My question is: why should I define these properties in the plain object since they are only getters? How can I create User
s using plain objects in this case?
Working code here
CodePudding user response:
When you write
const user: User = {
birthDate: new Date()
}
you do not create a User
object. You just create an object with a property birthDate
. This object does not fulfill the interface implied by the declaration of User
since it doesn't have the properties month
, year
and day
.
Do you know that Javascript doesn't really have classes? It only has objects. In pure old Javascript, where the wasn't a class
declaration, one wrote
function User(birthDate) {
this.birthDate = birthDate;
}
User.prototype.getYear = function () {
return this.birthDate.getYear();
}
User.prototype.getMonth = function () {
return this.birthDate.getMonth();
}
User.prototype.getDay = function () {
return this.birthDate.getDay();
}
var user = new User(new Date());
A function, here User()
could serve as a constructor, and when calling it with new
an object was created using the class' prototype
object as a prototype (actually, it's chained via the object's __proto__
property).
The class
declaration is merely syntactic sugar for the old pattern.
CodePudding user response:
The keyword here is new
. In your first example you're actually instancing a User
. In your second example you're merely creating an object that looks like a User (i.e. has all the attributes of a User).
You can create an object that looks like a user by providing all the attributes/getters/etc. However it's important to understand what the ramifications of this are. Note how the constructors are different.
class User {
birthDate: Date;
get month(): number {
return this.birthDate.getMonth();
}
get year(): number {
return this.birthDate.getFullYear();
}
get day(): number {
return this.birthDate.getDay();
}
}
const user1 = new User();
user1.birthDate = new Date(2010, 10, 22);
console.log("Constructor Name:" user1.constructor.name); //prints "Constructor Name: User":
const user2: User = {
birthDate: new Date(2010, 10, 22),
get month(): number {
return this.birthDate.getMonth();
},
get year(): number {
return this.birthDate.getFullYear();
},
get day(): number {
return this.birthDate.getDay();
},
}
console.log("Constructor Name:" user2.constructor.name); //prints "Constructor Name: Object"
Typescript playground demonstrating
CodePudding user response:
My question is: why should I define these properties in the plain object since they are only getters?
Because your type says that read-only year
, month
, and day
properties will exist on User
objects, but there aren't any on the object you're trying to assign to a variable of type User
.
How can I create Users using plain objects in this case?
By providing the getters:
const user: User = {
birthDate: new Date(),
get month(): number {
return user.birthDate.getMonth();
},
get year(): number {
return user.birthDate.getFullYear();
},
get day(): number {
return user.birthDate.getDay();
},
};
But using a constructor function instead seems reasonable, so you can reuse the getters on the prototype.
For completeness: You could also do it by having the raw object inherit from a prototype with those getters on; that's probably best wrapped up as a function:
interface User {
birthDate: Date;
readonly month: number;
readonly year: number;
readonly day: number;
}
const UserGetters: Partial<User> = {
get month(): number {
return this.birthDate!.getMonth();
},
get year(): number {
return this.birthDate!.getFullYear();
},
get day(): number {
return this.birthDate!.getDay();
}
};
function makeUser(init: Pick<User, "birthDate">): User {
return Object.assign(Object.create(UserGetters), init);
}
const user = makeUser({birthDate: new Date()})
console.log(user.year); // 2022 (at the moment)
(I cheated slightly there with non-null assertions, since I knew the makeUser
function wouldn't allow birthDate
to be unspecified or null
.)
Some folks don't like constructor functions but do like prototypes, so that's an option for them.