Home > Mobile >  Calling class prototype methods via index with TypeScript
Calling class prototype methods via index with TypeScript

Time:12-04

I would like to be able to call class prototype methods using bracket notation, so that the method name can be decided at run time:

classInstance['methodName'](arg);

I am failing to do this properly with TypeScript:

class Foo {
  readonly ro: string = '';
  constructor() {}
  fn(s: number) { console.log(s); }
}

const foo = new Foo();
const methods = ['fn'];

foo['fn'](0)

// Type 'undefined' cannot be used as an index type.
foo[methods[0]](1);

// This expression is not callable.
// Not all constituents of type 'string | ((s: number) => void)' are callable.
// Type 'string' has no call signatures.
foo[methods[0] as keyof Foo](1);

The above example is in the TS Playground.

I think that I have a reasonable understanding of what the errors mean and why the string literal in foo['fn'](0) does not produce an error. However, I don't understand how to prevent the errors. I thought that I might be able to use Extract to build a type comprising of Function, but I've failed to do that.

How can I produce a list of typed method names over which my code can iterate? And better, is it possible for the class to export such a list so that users of the class can easily access them?

Background Information

I have a Playwright test that needs to iterate over a list of methods from a Page Object Model, producing a screenshot for each.

CodePudding user response:

When you write

const methods = ['fn'];

The compiler infers the type of methods as string[], which means it may contain any number of any strings at all. So the compiler does not keep track of exactly which values are in the array, or where they are. This allows you to do things later like

methods.push("hello");

Often, this is what people want when they initialize a variable. But in your case, it is a problem, because then methods[0] could be any string whatsoever (or undefined if you have the --noUncheckedIndexedAccess compiler option enabled).


If you want the compiler to keep track of the exact literal types of the values in the array, the easiest way to do so is with a const assertion:

const methods = ['fn'] as const;

This tells the compiler that you would like to treat methods as essentially unchanging, and that it should infer the most specific type it can, more or less. Now methods is inferred to be of type

// const methods: readonly ["fn"]

which means that the compiler knows that methods is a readonly tuple containing exactly one element, whose type is the string literal type "fn".

So now the compiler knows that methods[0] is "fn", and your call compiles with no error:

foo[methods[0]](1); // okay

Playground link to code

CodePudding user response:

You need to make sure that your methods array is typed as an array containing only valid method names:

const methods: ('fn' | …)[] = ['fn'];

Notice that

const methods: (keyof Foo)[] = ['fn'];

doesn't cut it because Foo has also other keys (e.g. ro) that are not the names of methods, or the names of methods with a different signature than you need.

You can also just use

const methods = ['fn'] as const;
  • Related