Home > Software engineering >  TypeScript getter required when creating instance with plain object
TypeScript getter required when creating instance with plain object

Time:02-18

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 Users 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();
    },
};

Playground link

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)

Playground link

(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.

  • Related