I'm refactoring/rebuilding an existing codebase, and came across the equivalent of this code:
class FeedbackBase {
Variables = [{Value: 'doSomething'}];
goToFeedback() {
this.Variables?.forEach(variable => {
if (variable.Value) {
variable.Value = (this[variable.Value])();
}
});
}
doSomething(): string { return '123';}
}
The basic idea is that Variables[].Value might contain a function call defined on FeedbackBase. If it does, then run the function, and replace Variables[].Value with the return value.
Before this.Variables?.forEach, they were originally doing a deep copy of this.Variables, wiping out all of the typing and basically running the code on an 'any' object. When we keep the typing, Typescript complains about this[variable.Value]:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'FeedbackBase'. No index signature with a parameter of type 'string' was found on type 'FeedbackBase'.
Fair enough: all Typescript knows is that the contents of variable.Value are a string: it doesn't know whether it is a callable function on FeedbackBase.
My potential solution:
type BaseFunction = {
doSomething(): string;
};
class FeedbackBase implements BaseFunction {
Variables = [{Value: 'doSomething'}];
goToFeedback() {
this.Variables?.forEach(variable => {
if (variable.Value) {
variable.Value = this[variable.Value as keyof BaseFunction]();
}
})
}
doSomething(): string { return '123';}
}
Basically:
- Within
BaseFunction, explicitly define all of the functions that could be defined invariable.Value FeedbackBase implements BaseFunction- Call the function as
this[variable.Value as keyof BaseFunction]to tell Typescript that this should be treated as a function call, not as a string.
A nice feature of this approach is that when you say this[variable.Value as keyof BaseFunction], Typescript checks to make sure that this indeed has all of the functions defined in BaseFunction. (For example, if you change FeedbackBase.doSomething to FeedbackBase.doSomethings, an error appears at this[variable.Value as keyof BaseFunction]).
I think that, in this case, I'm forced to use as, because variable.Value could be keyof BaseFunction, or whatever is returned by the function call. Even if I get more specific about the return type, I can't think of a way to define variable.Value as definitely a function call before the transformation, and definitely a return type afterward, outside of just using as.
Is there a way to be more explicit?
FeedBackBaseclass receive itsVariablesarray?, what kind of mistakes should Typescript gard you against? ShouldVariablesbe allowed to containPropertieswhich don't exist in the instance? I agree with Jack but that's just me on auto-pilot, I don't understand what I am reading