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. :-)
If you have initializers on all the properties
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,
});
Playground Link
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;
}
}
Playground link
If you don't want to have to have those initializers
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>>'.
});
Playground link