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
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;