Search code examples
typescript

Return functions with different signatures from other function with correct typing


In my code I have a dispatch function that takes one parameter and returns another function depending on the parameter, something like this:

type Reason = 'create' | 'delete';

function dispatch(reason: Reason) {
    switch(reason) {
        case 'create':
        return createInternal;
        case 'delete':
        return deleteInternal;
    }
}

function createInternal(name: string) {
    console.log(name);
}

function deleteInternal(code: number) {
    console.log(code);
}

Now this initially worked, since my returned functions had the same signature. After adding another function with a different signature, I realised that this has an issue. The return type of the 'dispatch' function is a union of all possibly returned functions.

The following usage is now not possible anymore, since typescript can't infer the correct type of the returned function.

dispatch('create')('123'); // Argument of type 'string' is not assignable to parameter of type 'never'.
dispatch('delete')(123); // Argument of type 'number' is not assignable to parameter of type 'never'.

Is there a way to fix this, or alternatively implement a better dispatch function that doesn't have this issue?

Here's a link to a playground.

Edit

I have found half a solution with the help of generics. Now I can correctly call the dispatched function, but I get further errors inside the dispatch function that I don't understand.

Playground

type Reason = 'create' | 'delete';
type DispatchedFunction<T extends Reason> = T extends 'create' ? typeof createInternal : typeof deleteInternal;

function dispatch<R extends Reason>(reason: R): DispatchedFunction<R> {
    switch(reason) {
        case 'create':
        // Type '(name: string) => void' is not assignable to type 'DispatchedFunction<R>'.
        return createInternal;
        case 'delete':
        // Type '(code: number) => void' is not assignable to type 'DispatchedFunction<R>'.
        return deleteInternal;
    }
    return () => {} // I had to add this so the function doesn't return undefined
}

Solution

  • You could create a constant lookup object and just index into that:

    type Reason = 'create' | 'delete';
    
    const DispatchFunctions = {
        'create': createInternal,
        'delete': deleteInternal,
    } as const;
    
    function dispatch<R extends Reason>(reason: R): typeof DispatchFunctions[R] {
        return DispatchFunctions[reason];
    }
    
    function createInternal(name: string) {
        console.log(name);
    }
    
    function deleteInternal(code: number) {
        console.log(code);
    }
    
    dispatch('create')('123');
    dispatch('delete')(123);
    

    The same approach works when converting Reason to an enum:

    enum Reason {
        CREATE = 'create',
        DELETE = 'delete',
    }
    
    const DispatchFunctions = {
        [Reason.CREATE]: createInternal,
        [Reason.DELETE]: deleteInternal,
    } as const;
    
    function dispatch<R extends Reason>(reason: R): typeof DispatchFunctions[R] {
        return DispatchFunctions[reason];
    }
    
    function createInternal(name: string) {
        console.log(name);
    }
    
    function deleteInternal(code: number) {
        console.log(code);
    }
    
    dispatch(Reason.CREATE)('123');
    dispatch(Reason.DELETE)(123);