ken
ken

Reputation: 8993

Dynamic interface definition

I have abstract base class for a bunch of "schema definition" classes:

xyz extends BaseSchema

What I want to ensure is that the parent schema classes are exporting meaningful constraints/interfaces for the schema's properties (and eventually for it's relationships). So right now the base class is defined as:

export abstract class BaseSchema {
  /** The primary-key for the record */
  @property public id?: string;
  /** The last time that a given model was updated */
  @property public lastUpdated?: datetime;
  /** The datetime at which this record was first created */
  @property public createdAt?: datetime;
  /** Metadata properties of the given schema */
  public META?: Partial<ISchemaOptions>;

  public toString() {
    const obj: IDictionary = {};
    this.META.properties.map(p => {
      obj[p.property] = (this as any)[p.property];
    });
    return JSON.stringify(obj);
  }

  public toJSON() {
    return this.toString();
  }
}

The an example parent class might look like so:

export class Person extends BaseSchema {
  // prettier-ignore
  @property @length(20) public name: string;
  @property public age?: number;
  @property public gender?: "male" | "female" | "other";
  // prettier-ignore
  @property @pushKey public tags?: IDictionary<string>;

  // prettier-ignore
  @ownedBy(Person) @inverse("children") public motherId?: fk;
  // prettier-ignore
  @ownedBy(Person) @inverse("children") public fatherId?: fk;
  @hasMany(Person) public children?: fk[];

  @ownedBy(Company) public employerId?: fk;
}

Now you'll notice that I am using Typescript's support for decorators and in this case the main thing to understand about the decorator code is that the @property decorator is used to explicitly state a property on the schema. This stamps meta information about the properties of the schema into a dictionary object on the schema class's META.proproperties. You'll see this is used later on in the toString() method. If you feel the actual decorator code is important to understand you can find it HERE.

What I want to be able to do is expose an interface definition that constrains the type to the properties and types of those properties defined in the schema. Something like the following:

 function foobar(person PersonProperties) { ... }

so that the property "person" would be constrained by the properties that are required and that the intellisense in the editor would offer both required (aka, "name") and unrequired properties (aka, "age", "createdAt", etc.) defined as properties.

Eventually I'd like to export a type that does the same thing for relationships but I'd be happy to just get the schema's properties for now.

Upvotes: 2

Views: 727

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249536

In typescript 2.8 you can use conditional types and mapped types to create a type that contains only the fields of the class not the methods, and preserves the required/optional attribute of the field:

type NonMethodKeys<T> = {[P in keyof T]: T[P] extends Function ? never : P }[keyof T];  
type RemoveMethods<T> = Pick<T, NonMethodKeys<T>>; 

class Person {
  public name: string;
  public age?: number;
  public gender?: "male" | "female" | "other";
  toJson(): string { return ''}
}

let ok: RemoveMethods<Person> = {
  name : 'Snow',
  gender: 'male'
};

// Error no name
let nokNoName: RemoveMethods<Person> = {
};

let nok: RemoveMethods<Person> = {
  name : 'Snow',
  gender: 'male',
  toJson(): string { return ''}  // error unknown property
};

If you are using Typescript 2.7 or lower, there are no conditional types, and you can only use mapped type such as Pick or Partial. For a discussion as to their relative advantages you can see this question

Unfortunately there is no way to use decorators to discern what fields make it into the interface, we can only use the type of the field. You might consider a design where you class only has fields that are also properties, or where such fields are held in a special class that contains only properties.

There is always the option of defining an extra interface with just the properties explicitly but this will add extra maintenance hassle.

Upvotes: 2

Related Questions