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