Reputation: 1641
I like in Typescript interfaces doing this:
interface Person {
name: string;
age: number;
}
const person: Person = {
name: "Foo";
age: 23;
}
because is very coincise and clean. However, suppose I need to add a method isAdult which makes sense on a Person. I cannot add a method to the interface, so I need to convert it to a class:
class Person {
public name: string;
public age: number;
isAdult() {
return this.age >= 18;
}
}
But now, how can I instanciate this class using the object person defined above? I cannot do:
const person: Person = new Person({
name: "Foo";
age: 23;
})
I don't want to explicitly write the class constructor with all the verbosity with the constructor arguments and the explicit set of each property, nor to have to pass Person("Foo", 23) explicitly in that order without being explicitly about what is Foo and what is 23.
So is there a way to instanciate the Person class from the person object, being as coincise and short as possible?
Upvotes: 1
Views: 3786
Reputation: 1074385
You're swimming upstream a bit here. :-) But you've ruled out the usual thing, which would be a constructor(public name: string, public age: number)
or similar, so...
Just to set the stage, there's no way to just assign a plain object to a variable with a class's type and have it get hooked up to the class's prototype methods. You need to call the constructor. E.g., you need new Person
. (On the up side, you don't need to declare the type; TypeScript will infer it. So it's almost as concise, you just need to type new
and a couple of ()
[and don't need the :
].)
You've said:
I cannot do:
const person: Person = new Person({ name: "Foo"; age: 23; })
I don't want to explicitly write the class constructor with all the verbosity with the constructor arguments and the explicit set of each property...
...but doing that doesn't require writing a constructor explicitly setting the properties.
Strap in, this is a bit of a ride. :-)
In that case, it can be as simple as:
constructor(props?: Partial<Person>) {
Object.assign(this, props);
}
E.g.:
class Person {
name: string = "";
age: number = NaN;
constructor(props?: Partial<Person>) {
Object.assign(this, props);
}
isAdult() {
return this.age >= 18;
}
}
Notice the property initializers. They're important, because you can't guarantee that all calls to the constructor will supply all properties.
You use it like this:
const p = new Person({
name: "Foo",
age: 23,
});
That constructor does check what you pass in, this would be rejected:
const p2 = new Person({
frog: "Foo", // // Argument of type '{ frog: string; age: number; }' is not assignable to parameter of type 'Partial<Person>'.
age: 23,
});
If you didn't want to have to write that constructor every time, you could write a base class:
class Base<T> {
constructor(props?: Partial<T>) {
Object.assign(this, props);
}
}
Then the class itself looks like this:
class Person extends Base<Person> {
name: string = "";
age: number = NaN;
isAdult() {
return this.age >= 18;
}
}
This gets really fun. We want to have a way of requiring the non-function properties, some kind of
constructor(props: OmitFunctions<Person>)
I figured we could probably get there with a mapped type, and thanks to this answer (thank you Titian Cernicova Dragomir!), we can:
type NotKeyOfType<T, U> = {[P in keyof T]: T[P] extends U ? never : P}[keyof T];
type OmitFunctions<T> = Pick<T, NotKeyOfType<T, Function>>;
Now we can use OmitFunctions<Person>
instead of Partial<Person>
. Here's the base class (this time, props
isn't optional — you might have a base class for classes that have all optional properties that makes it optional, and one for classes with required properties that doesn't):
class Base<T> {
constructor(props: OmitFunctions<T>) {
Object.assign(this, props);
}
}
Then the Person
(et. al.) classes:
class Person extends Base<Person> {
name!: string;
age!: number;
isAdult() {
return this.age >= 18;
}
}
Notice the !
. In that position, it's a definite assignment assertion. What we're saying is "Hey TypeScript, I know you can't see it happen, but these do get initialized."
Usage:
const p = new Person({
name: "Foo",
age: 23,
});
Invalid properties fail:
const p2 = new Person({
frog: "Foo", // Argument of type '{ frog: string; age: number; }' is not assignable to parameter of type 'Pick<Person, NotKeyOfType<Person, Function>>'.
age: 23,
});
And missing properties fail:
const p3 = new Person({
age: 23, // Argument of type '{ age: number; }' is not assignable to parameter of type 'Pick<Person, NotKeyOfType<Person, Function>>'.
// Property 'name' is missing in type '{ age: number; }' but required in type 'Pick<Person, NotKeyOfType<Person, Function>>'.
});
Upvotes: 4
Reputation: 1635
What you can do here is extend the class to an interface
class Person {
isAdult(age) {
return age >= 18;
}
}
interface IPerson extends Person {
name: string;
age: number;
}
and then you can create variables using the IPerson interface
Check the out the docs here for more info. https://www.typescriptlang.org/docs/handbook/interfaces.html#interfaces-extending-classes
Or you can create a function declaration in your interfaces and then define that function logic inside your class can also do
interface IPerson extends Person {
name: string;
age: number;
isAdult():boolean;
}
Upvotes: 0