3

I want to define a TypeScript class that will receive it's properties via a plain object (like a database document).

class Car {
   color: string;
   
   constructor(props: ???) {
       Object.assign(this, props)
   }
 
}

What is the best way to type props?

I could create another interface, called CarProps and define them there, like:

class Car implements CarProps { 
    constructor(props: CarProps) {
        Object.assign(this, props)
    }
}

I am struggling with what to name the interface however, since the TypeScript Handbook forbids starting interface names with "I" (e.g. ICar). Since I would end up with an interface and class that define "Car", they both cant have the same name, so one of them has to be offset like "CarProps", "ICar", or "CarClass".

A problem with this approach is that the properties must be defined twice:

interface CarProps {
  color: string;
}

class Car implements CarProps {
  color: string; // duplication here
}

Some solutions are offered here (interface merging and abstract classes): Why the need to redefine properties when implementing an interface in TypeScript?

Would appreciate any suggestions on the best practice for this approach.

3 Answers3

2

I think the CarProps approach is good, if that helps.

Calling it ICar or Car wouldn't make sense, but this is assuming that your class will have some other public fields/methods other than those in the interface you're creating.

If this is not the case, and you want some replica of fields in Car, then you could use Partial<Car>.

The problem with this is that it will include all public properties and methods in the Partial - so you allow the case where the method vroom can be overridden (see car2 below).


class Car {
  foo: number = 123;

  constructor(props: Partial<Car>) {
    Object.assign(this, props);
  }

  vroom = () => {
    console.log('vroom');
  }
}

const car = new Car({ foo: 567 });

// Should this be allowed?
const car2 = new Car({ foo: 567, vroom: () => console.log('hello') });


Pytth
  • 4,008
  • 24
  • 29
mbdavis
  • 3,861
  • 2
  • 22
  • 42
1

You can also make sure of TypeScript's implements keyword, which allows you to specify members in the class, which you can also use to type the incoming props argument. If props is not a partial field (aka all fields must be specified in the object), you can simply do this (see example on TypeScript playground):

interface Vehicle {
  color: string;
  model: string;
}

class Car implements Vehicle {
  color!: string;
  model!: string;
  
  constructor(props: Vehicle) {
    Object.assign(this, props)
  }

  public start(): void {
    console.log(`Staring ${this.model} that has the color ${this.color}`);
  }
}

const blackHonda = new Car({ color: 'black', model: 'Honda' });
blackHonda.start();

In this case, all fields in the Vehicle interface must be specified in the object passed into the constructor of the Car class.

However, if you wish for a partial interface, then it is a good idea to provide defaults, although it doesn't really make a lot of sense in this example (but might make sense in your implementation):

interface Vehicle {
  color: string;
  model: string;
}

class Car implements Vehicle {
  color: string = 'white';
  model: string = 'Tesla';
  
  constructor(props?: Partial<Vehicle>) {
    Object.assign(this, props)
  }

  public start(): void {
    console.log(`Staring ${this.model} that has the color ${this.color}`);
  }
}

const blackHonda = new Car({ color: 'black', model: 'Honda' });
blackHonda.start();

const defaultCar = new Car();
defaultCar.start();

See example on TypeScript playground.

Terry
  • 63,248
  • 15
  • 96
  • 118
  • My problem with Partial<> is that is makes everything optional, while it is useful that it validates properties, it won't enforce the required ones. My other issue here is that when inheriting from an interface, the properties must be defined twice (model and color). – Andrew Becker Dec 29 '20 at 22:10
  • If some car properties are required, you could move them to the arguments of `Car.constructor`, and keep the `props` argument for all optional properties. Child classes would then be forced to pass them explicitly. – Blaise May 07 '21 at 13:42
1

I wanted to avoid the need for a separate interface that would require duplicating the property definitions (once for the class, once for the interface).

I found this custom TypeScript type:

type NonFunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? never : K
}[keyof T]
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>

And it allowed me to make my class constructors like this:

constructor(props: NonFunctionProperties<Car>) {
  Object.assign(this, props)
}

So now I can do new Car(plainCarObject)! And plainCarObject will be properly type checked again all non-function properties on the class.