The code below explains issue better than words. It seems to me that it should work, but for some reason it doesn't. What's going on here?
interface Types {
'type1': boolean,
'type2': string,
}
type TypesCallbacks = {
[key in keyof Types]: (object: any) => Types[key];
};
class TypeProvider {
private callbackMap: TypesCallbacks = {
type1: (object) => false,
type2: (object) => 'test',
};
public runTypeCallback<T extends keyof TypesCallbacks>(id: T): ReturnType<TypesCallbacks[T]> {
const callback = this.callbackMap[id];
return callback('zz');
//^^^^^^^^
//There is an error here
}
public getCallback<T extends keyof TypesCallbacks>(id: T): TypesCallbacks[T] {
return this.callbackMap[id];
//^^^^^^^^^^^^^^^^^^^
//But this works! Which is proven below.
}
}
const provider: TypeProvider = new TypeProvider();
const booleanValue: boolean = provider.runTypeCallback('type1');
const stringValue: string = provider.runTypeCallback('type2');
const booleanCallback: (object: any) => boolean = provider.getCallback('type1');
const stringCallback: (object: any) => string = provider.getCallback('type2');
CodePudding user response:
The problem is, that inside the runTypeCallback
body, callback
's currently type isn't restricted to one T
inference or the other - instead it is both.
If you hover on callback
in your editor, you can see it infers this type:
(object: any) => string | boolean
Which fails type1
because it contains string
, and fails type2
because of boolean
.
You will have to assert one or the other, or the final type, which would be a supertype of the actual type, so it should work.
Just add:
as ReturnType<TypesCallbacks[T]>
at the end of return
, like so:
public runTypeCallback<T extends keyof TypesCallbacks>(id: T): ReturnType<TypesCallbacks[T]> {
const callback = this.callbackMap[id];
return callback('zz') as ReturnType<TypesCallbacks[T]>;
}
}
You can see it working in the playground.
CodePudding user response:
The generic type T
is not "resolved" at the definition site, but at the call site. At definition site, T
type is keyof TypesCallbacks
, not "type1"
or "type2"
.
in the getCallback
method, id
type is keyof TypesCallback
, which is a valid type, since id
is used as a key for callbackMap
object
public getCallback<T extends keyof TypesCallbacks>(id: T): TypesCallbacks[T] {
// id type is keyof TypesCallbacks
// id type is perfectly valid -> no error
return this.callbackMap[id];
}
in the runTypeCallback
method, T
type is also keyof TypesCallbacks
, neither "type1"
or "type2
. this means that:
- the
callback
type is not narrowed byid
. thecallback
type isTypesCallbacks[T]
. - and the
ReturnType<TypesCallbacks[T]>
doesn't infer the return type of the selected callback correctly.
public runTypeCallback<T extends keyof TypesCallbacks>(id: T): ReturnType<TypesCallbacks[T]> { // not infered as string or boolean but as ReturnType<TypesCallbacks[T]>
// callback type is TypesCallbacks[T]
// neither (object: any) => boolean
// or (object: any) => string as expected
const callback = this.callbackMap[id];
return callback('zz');
}
To solve the problem you need to use type assertion.