Typescript – Enhance an object and its type at runtime

Typescript: enhance an object’s properties and methods at runtime, by extending also its type

In Javascript it’s very common to manipulate objects at runtime by adding/deleting properties and methods.

Suppose to have an object like this:

const formulas = {
  f1: "Formula1",
  f2: "Formula2",
  f3: "Formula3"
};

And suppose you want to add a method to print all the fields separated with a custom separator:

formulas['printFields'] = function(separator) {
    return Object.keys(this)
      .filter(key => typeof this[key] !== 'function')
      .map(prop => this[prop])
      .join(separator);
};

The output of:

formulas.printFields(' | ');

would be the string

"Formula1 | Formula2 | Formula3"

What if we wanted to accomplish such an enhancement in Typescript, by enhance also the object type?
Let’s think about working with models for database tables.

export class GenericTable {
  __TABLENAME__: string;

  constructor(tableName: string) {
    this.__TABLENAME__ = tableName;
  }
};

Models for database tables are classes that extend GenericTable class and have properties representing the table’s fields.

export class UsersTable extends GenericTable {
  ID: string;
  NAME: string;
  SURNAME: string;
  EMAIL: string;
  
  constructor() {
    super('users');
    // assign the real fields names
    this.ID = 'id';
    this.NAME = 'name';
    this.SURNAME = 'surname';
    this.EMAIL = 'email';
  }
};

The goal is to add a get method for each property, which returns the field name preceded by the table name.
We could simply write a table enhancer to insert the get methods in this way:

export function simpleTableEnhancer<Type extends GenericTable>(tableObj: Type): Type {
  for (const key of Object.keys(tableObj)) {
    if (key !== '__TABLENAME__') {
      tableObj[`get${key}`] = function(): string {
        return `${tableObj.__TABLENAME__}.${tableObj[`${key}`]}`;
      };
    }
  }
  return tableObj;
}

This function would correctly add the get methods for each property of the object passed in input (not considering the property __TABLENAME__), but what would happen if we wanted to access those runtime created methods?

The ‘get’ methods added by the simpleTableEnhancer are not available in the autocompletion

As shown in the previous image, the autocompletion would not contain the runtime added methods, as the type returned by the simpleTableEnhancer is the exact input type (which does not contain those methods).
Even if we explicitly wrote the method, Typescript would return an error:

The method getNAME is not found

The solution to this problem must also include the enhancement of the object type, and I took advantage of Typescript’s Mapped Types and in particular of the key-remapping feature.
The function coolTableEnhancer is responsible for this: not only it adds the get methods for each property at runtime, but it also returns the modified object by casting it to the enhaced version of its type.

export type Get<Type> = {
  [Property in keyof Omit<Type, "__TABLENAME__"> as
   `get${Capitalize<string & Property>}`]: () => Type[Property]
};
export type EnhancedTable<Type> = Type & Get<Type>;

export function coolTableEnhancer<Type extends GenericTable>(tableObj: Type):
  EnhancedTable<Type> {
  for (const key of Object.keys(tableObj)) {
    if (key !== '__TABLENAME__') {
      tableObj[`get${key}`] = function(): string {
        return `${tableObj.__TABLENAME__}.${tableObj[`${key}`]}`;
      };
    }
  }
  return tableObj as EnhancedTable<Type>;
}

The type Get<Type> is responsible to create a new type, composed of only get methods for each property found in the base type passed in input (excluded __TABLENAME__).
The following Typescript Playground example shows clearly the type created by applying Get<UsersTable>.

Typescript playground example to show the type created by applying Get<UsersTable>

Finally EnhancedTable<Type> uses the Intersection Types construct to combine the Type passed in input and the type generated by applying Get<Type>.


The following image clearly shows that the object returned by the function coolTableEnhancer can be used to access all the new added get methods.

All the methods added by the coolTableEnhancer are correctly available in the autocompletion

Check the full code at: https://bitbucket.org/danilocarrabino_xriba/enhanceme

Total
0
Shares
Leave a Reply

Your email address will not be published.

Previous Post

From a legacy UI to a multi-app Design System

Next Post

Demystifying the “docker build” command