Vahid Hallaji
Vahid Hallaji

Reputation: 7447

How to let typescript understand the existence of static members that are dynamically added to a class?

I've got a QueryBuilder and would like to present all its methods as static member on the Model class.

Apparently, It's valid Javascript code. However, how could I let typescript understand the existence of those static methods on Model?

function queryable (target: any) {
  for (const prop in target.prototype.builder) {
    if (prop === 'constructor' || target.prototype.hasOwnProperty(prop)) {
      continue;
    }
    target[prop] = (...args: any[]): any => {
      return target.prototype.builder[prop](...args);
    };
  }
}

class QueryBuilder {
  where(prop: string, value: string) {
    console.log(prop, value);
    console.log("I've presented on model class as static method.");
  }
  // ...
}

@queryable
class Model {
  get builder () {
    return new QueryBuilder();
  }
  // ...
}


// calling static where
Model.where('color', 'red');

Property 'where' does not exist on type 'typeof Model'.

See in Typescript Playground

Upvotes: 2

Views: 158

Answers (2)

SpencerPark
SpencerPark

Reputation: 3506

The issue you should keep an eye on is TypeScript#4881 but as of currently (3.5.1) a decorator can't change the type of the class it is decorating.

That being said you can still define the decorator in the same fashion but call it explicitly rather than as a decorator, as many typescript projects using redux's popular connect decorator have to do.

class _Model {
  get builder () {
    return new QueryBuilder();
  }

  // ...
}
const Model = queryable(_Model);

or if it is the main export of a module you can get away with one name:

class Model {
  get builder () {
    return new QueryBuilder();
  }

  // ...
}
export default queryable(Model);

Then the trick is to better annotate the decorator to teach typescript all the additions queryable has made to it's target:

function queryable<T extends new (...args: any[]) => {builder: any}>(target: T): InstanceType<T>['builder'] & (typeof target) {
  for (const prop in target.prototype.builder) {
    if (prop === 'constructor' || target.prototype.hasOwnProperty(prop)) {
      continue;
    }
    (target as any)[prop] = (...args: any[]): any => {
      return target.prototype.builder[prop](...args);
    };
  }
  return target as any
}

Playground link

Upvotes: 2

Ken Bekov
Ken Bekov

Reputation: 14015

Since Typescript is a strong typing language it always checks if type usage matches type interface. This checking takes place during transpilation, when Typescript has no idea you're going to add a new method. Because you add it in run-time. The only way to call such a method is casting to something general like any:

(<any>Model).where('color', 'red');

It's like you're saying to Typescript that you're sure about method existence and transpiller shouldn't worry about it.

Upvotes: 0

Related Questions