Search code examples
typescripttypescript-genericstype-inference

How can I infer the generic type parameters of a generic parameter in typescript?


I have a type that represents an operation in my application:

type Operation<A, B> = {
    execute: (data: A) => B;
};

An operation looks like this:

const op = {
    execute: (data: string) => {
        return data;
    },
};

I'm trying to create an abstraction that lets me decorate this operation. Adding a decoration should preserve any previously added fields. So for example when I want to augment an operation with transaction handling:

export const wrapInTransaction = <A, B, O extends Operation<A, B>>(
   { execute, ...rest}: O 
): O => {
    const wrappedExecute = (data: A) =>
    {
        // add transaction logic here
        return execute(data)
    }        
    return {
        ...rest,
        execute: wrappedExecute,
    } as O;
}

I'm accepting a subtype of Operation, not a generic Operation<A, B>. My problem is that this doesn't work:

const wrapped = wrapInTransaction(op);

// Argument of type '{ execute: (data: string) => string; }' is not assignable to parameter // of type 'Operation<unknown, unknown>'.
//   Types of property 'execute' are incompatible.
//     Type '(data: string) => string' is not assignable to type '(data: unknown) => unknown'.
//       Types of parameters 'data' and 'data' are incompatible.
//         Type 'unknown' is not assignable to type 'string'

as the type of O is not inferred. Can I somehow construct a default value for O that infers A and B?


Solution

  • I would recommend simplifying the generic. Given that it only applies O to the parameters { execute, ...rest}: O, it can only infer O. While deducing A and B is trivial for humans, we can do it rigorously using the infer keyword documented here: Conditional Types, to help us obtain A and B. Note that it cannot actually be in the generic, otherwise it is a circular type dependency.

    // VV this works as well
    // type DataFromOperation<T> = T extends Operation<infer A, unknown> ? A : unknown
    type DataFromOperation<T extends Operation<unknown, unknown>> = T extends Operation<infer A, unknown> ? A : unknown
    
    export const wrapInTransaction = <O extends Operation<any, any>>(
       { execute, ...rest}: O 
    ): O => {
        const wrappedExecute = (data: DataFromOperation<O>) =>
        {
            // add transaction logic here
            return execute(data)
        }        
        return {
            ...rest,
            execute: wrappedExecute,
        } as O;
    }
    

    View on TS Playground

    P.S. I didn't refresh the page before @jcalz answer, which is also perfectly valid and maybe even preferable, dependent on specific requirements (ie. whether you need to keep A, B and O as generic parameters).