3

I've some code like:

const methodsList = [
  'foo',
  'bar',
  // ... 20 other items ...
]

export class Relayer {
  constructor() {
    for (const methodName of methodsList) {
      this[methodName] = (...args) => {
        // console.log('relaying call to', methodName, args)
        // this is same for all methods
      }
    }
  }
}

const relayer = new Relayer()

relayer.foo('asd') // TS error
relayer.bar('jkl', 123) // TS error

Now when I use the class instance, TypeScript complains when I call relayer.foo() or relayer.bar(). To make the code compile, I've to cast it as any or similar.

I've an interface that declares foo, bar and the other methods:

interface MyInterface {
  foo: (a: string) => Promise<string>
  bar: (b: string, c: number) => Promise<string>
  // ... 20 other methods
}

How do I get TypeScript to learn the dynamically declared foo and bar class methods? Can the declare syntax be useful here?

3 Answers 3

8
+50

First step is to create a type or interface where when indexed by a value in methodsList, the result will be a function:

// The cast to const changes the type from `string[]` to
// `['foo', 'bar']` (An array of literal string types)
const methodsList = [
    'foo',
    'bar'
] as const

type HasMethods = { [k in typeof methodsList[number]]: (...args: any[]) => any }

// Or
type MethodNames = typeof methodsList[number]  // "foo" | "bar"
                   // k is either "foo" or "bar", and obj[k] is any function
type HasMethods = { [k in MethodNames]: (...args: any[]) => any }

Then, in the constructor, to be able to assign the keys of methodsList, you can add a type assertion that this is HasMethods:

// General purpose assert function
// If before this, value had type `U`,
// afterwards the type will be `U & T`
declare function assertIs<T>(value: unknown): asserts value is T

class Relayer {
    constructor() {
        assertIs<HasMethods>(this)
        for (const methodName of methodsList) {
            // `methodName` has type `"foo" | "bar"`, since
            // it's the value of an array with literal type,
            // so can index `this` in a type-safe way
            this[methodName] = (...args) => {
                // ...
            }
        }
    }
}

Now after constructing, you have to cast the type still:

const relayer = new Relayer() as Relayer & HasMethods

relayer.foo('asd')
relayer.bar('jkl', 123)

You can also get rid of the casts when constructed using a factory function:

export class Relayer {
    constructor() {
        // As above
    }

    static construct(): Relayer & HasMethods {
        return new Relayer() as Relayer & HasMethods
    }
}

const relayer = Relayer.construct()

Another way around it is to create a new class and type-assert that new results in a HasMethods object:

class _Relayer {
    constructor() {
        assertIs<HasMethods>(this)
        for (const methodName of methodsList) {
            this[methodName] = (...args) => {
                // ...
            }
        }
    }
}

export const Relayer = _Relayer as _Relayer & { new (): _Relayer & HasMethods }

const relayer = new Relayer();

relayer.foo('asd')
relayer.bar('jkl', 123)

Or if you are only using new and then methods in methodsList, you can do:

export const Relayer = class Relayer {
    constructor() {
        assertIs<HasMethods>(this)
        for (const methodName of methodsList) {
            this[methodName] = (...args) => {
                // ...
            }
        }
    }
} as { new (): HasMethods };

You can also use your MyInterface interface instead of HasMethods, skipping the first step. This also gives type-safety in your calls.

Sign up to request clarification or add additional context in comments.

Comments

1

Use the following syntax:

export class Relayer { 
  constructor() {}
  public foo(){
    // your foo method
    this.executedOnEachFunction();
  }
  public bar(){
    // your bar method
    this.executedOnEachFunction();
  }
  executedOnEachFunction(){
    // what you want to do everytime
  }
}

https://repl.it/repls/LawfulSurprisedMineral

4 Comments

Thanks for the answer. That does work. But I can't use it because I'll have to define the same function ~20 times with different names.
You can declare it once in your class and call it on every function of your class.
@Elmo When you implement an interface you need at least to declare each method, this is the contract. It's like in Java and C#. Take a look at typescriptlang.org/docs/handbook/interfaces.html#class-types
I'm declaring each method in the interface but dynamically. It's almost the same thing but with different syntax. If only type script allowed me to override the types arbitrarily...
0

To me, this sounds like a need for an interface.

interface MyInterface {
 foo(): void; // or whatever signature/return type you need
 bar(): void;
  // ... 20 other items ...
}

export class Relayer implements MyInterface {
  constructor() {}

  foo(): void {
    // whatever you want foo to do
  }

  // ... the rest of your interface implementation
}

What it looks like you are doing is implementing some interface of sorts. In your constructor you are defining what the method implementations are instead of defining them in the class body. Might help to read Class Type Interfaces

1 Comment

I tried implements Interface but TypeScript fails to compile and asks me to define the dynamically declared methods statically.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.