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
callbacktype is not narrowed byid. thecallbacktype 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.
