edoedoedo
edoedoedo

Reputation: 1641

Map object to class properties like interfaces

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

Answers (2)

T.J. Crowder
T.J. Crowder

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. :-)

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

Upvotes: 4

Hitech Hitesh
Hitech Hitesh

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

Related Questions