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:
BaseFunction
, explicitly define all of the functions that could be defined in variable.Value
FeedbackBase implements BaseFunction
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?
I don't know if this is the best TypeScript per say because it seems a bit convoluted, but I think your solution is fine in terms of typings. I would beware of one thing though.
I don't think the forEach
does what you want it to. If a Value
is not a function, you'll get a runtime error because you're not actually checking if Value
is a function. Instead, you're checking if it exists.
this.Variables?.forEach((variable) => {
if (
variable.Value &&
variable.Value in this &&
typeof this[variable.Value as keyof BaseFunction] === "function"
) {
variable.Value = this[variable.Value as keyof BaseFunction]()
}
})
TS would not allow you to do that if it knew what you wanted to do, and I think it would be right in complaining. This is what this answer will attempt to demonstrate.
You don't need an interface to wire the types. You can statically work out the names of FeedbackBase
non-void method names like so:
type NonVoidMethodNames<Class> = keyof {
[K in keyof Class
as ((...args: any[]) => void) extends Class[K] ? never
: Class[K] extends (...args: any[]) => unknown ? K
: never
]: K
}
type DoSomething = NonVoidMethodNames<FeedbackBase> // "doSomething"
If you assigned it to Variables
like the following, you would not need your as
when you call this[variable.Value]()
.
Variables: { Value: NonVoidMethodNames<FeedbackBase> }[] = [{ Value: 'doSomething' }];
TS would try to index this
with "doSomething"
, realise it's a method, no problem, and work out the return type as string
as expected
But of course we can't simply do that because now we're assigning string
to variable.Value
which we had defined to be "doSomething"
.
In TS, you can't write the following and this is the situation you are in, only with indirections in-between.
let foo: string = 'a';
foo = 1;
You would also be inconvenienced if you wanted to widen Value
to MethodName | ReturnType
MethodName | ReturnType
when the return type is string
as it is the case here because string
would swallow its subtypes.
Even ignoring the problem of when Variables
is populated, the user has no way to know what values this field contains. This is a deal breaker in my opinion. I am not very savvy in OO but I would say this design is flawed and cannot be typed.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.