Home > Software engineering >  ReturnType on a callback in a nested property doesn't work
ReturnType on a callback in a nested property doesn't work

Time:12-04

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

Playground Link

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 by id. the callback type is TypesCallbacks[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.

  • Related