0

We're currently adopting flow, and I'm running into an interesting problem. In one of our code-bases, we use a pattern roughly as follows.

We have 'models', which represent a 'Value object' or 'Data transfer object'. So our 'models' hold a single record. This is one of the simplest possible models:

class UserModel extends BaseModel {

}

The BaseModel defines a constructor, which roughly looks like this:

class BaseModel {

  constructor(properties: Object) {
    for (let key in properties) {
      this[key] = properties[key];
    }
  }
}

This allows you to do stuff like this:

const user = new UserModel({
  firstName: 'Evert',
  lastName: 'Pot'
});

I'm looking for an elegant way to add type-safety to this. One of the issues I'm facing is that I'd like both typesafety in places where an instance of UserModel is passed around, as well as during the construction of UserModel. Ideally I'd like UserModel to not be allowed to be instantiated in incomplete/invalid forms.

My first pass on this (which meets this goal) is this:

/* @flow */

interface UserInterface {

  firstName: string;
  lastName: string;

}

class UserModel extends BaseModel implements UserInterface {

   firstName: string;
   lastName: string;

   constructor(props: UserInterface) {
      this.firstName = props.firstName;
      this.lastName = props.lastName;
   }

}

const user = new User({
   firstName: 'Evert',
   lastName: 'Pot'
});

I'm re-using UserInterface both as a type for the constructor as well as the class itself.

This works, but has the drawback that I'm repeating every property name 4 times. When defining the interface, when defining the class and when setting the property in the constructor.

Being fairly new to flow, I wonder if there's a way to reduce this. Can I remove the duplication of property names?

Evert
  • 93,428
  • 18
  • 118
  • 189
  • 1
    I wonder if you really need all these classes? What's wrong with just `const user: UserInterface = { firstName: 'Evert', lastName: 'Pot' };` ? – Aleksey L. Feb 06 '18 at 06:47
  • @AlekseyL. good question. Our real use-case has a number of methods on various model classes. – Evert Feb 06 '18 at 18:19

1 Answers1

0

You could change your BaseModel to have an internal dictionary for properties like:

class BaseModel {
  props: Object = {}

  constructor(props: Object) {
    this.props = props
  }
}

You could even use a proxy to set up dynamic getters/setters like in this answer or iterate over your properties and use defineProperty (similar to this fiddle I whipped up. You could alternatively just add a get method or something that accesses those values for you (similar to Backbone).

Then you can just have your UserInterface as a type if you want and do the following:

type UserInterface = {
  firstName: string;
  lastName: string;
}

class UserModel extends BaseModel {
  // The constructor is only necessary for flow to limit the values allowed
  constructor(props: UserInterface) {
    super(props)
  }
}
kalley
  • 18,072
  • 2
  • 39
  • 36