Typescript keep type information mapping over object

Viewed 233

I have an object containing functions, which i have to wrap to use their return values, without changing their signature and while keeping type information.

// a object containing functions
const functions = { foo, bar, baz }

// example wrapper
const doSomething: (result: any) => void;

// wrapper function for each function
function wrapper<P extends any[]>(fn: (...params: P) => any) {
    return (...params: P) => doSomething(fn(...params));
}

// the wrap function, which needs type information
function wrap(functions) {
    const wrapped = {};

    for (const key in functions) {
        wrapped[key] = wrapper(functions[key]);
    }

    return wrapped;
}

const wrapped = wrap(functions);

// get type information for wrapped and its functions here

Is it possible to keep type information (list of functions and their parameters) while wrapping the functions?

2 Answers

TypeScript won't infer this higher-order type operation for you; you'll have to annotate and assert in some places. Inspecting the type of wrapper shows

/* function wrapper<P extends any[]>(fn: (...params: P) => any): (...params: P) => void */

so it returns a function whose parameters are the same as the input function, but which returns void. In this case I'd write wrap() like:

function wrap<T extends Record<keyof T, (...args: any) => any>>(functions: T) {
    const wrapped = {} as { [K in keyof T]: (...p: Parameters<T[K]>) => void };
    for (const key in functions) {
        wrapped[key] = wrapper(functions[key]);
    }
    return wrapped;
}

The functions argument is of generic type T which is constrained to an object type whose properties are functions. And the return wrapped is asserted to be of a mapped type which has the same property names as T, and whose value function types are the same as the corresponding property of T but where the return type has been changed to void. This uses the Parameters utility type to say that the output functions take the same parameters as the input functions.

I like to have complete examples when I write code for Stack Overflow, so here's the definitions I'm using:

const functions = {
    foo: (x: string) => x.length,
    bar: (x: number) => x.toFixed(2),
    baz: (x: boolean, y: boolean) => x && y;
}

const doSomething = (result: any) => { console.log(result) };

And then when I call wrap() we get the following behavior:

const wrapped = wrap(functions);
wrapped.foo("hey"); // 3
wrapped.bar(Math.PI); // 3.14
wrapped.baz(true, false); // false

If I try something wrong, the compiler complains:

wrapped.foo(true); // error! boolean is not a string
wrapped.foo("string", false); // error! expected 1 argument, got 2

Looks like what you want, I think.

Playground link to code

With the help of jcalz answer I made an more generic function objects mapper.

It wraps every function in the object with a given function, without changing the objects type signature.

function mapFunctionsObj<T extends Record<keyof T, (...p: any[]) => any>>(
    fn: <X>(result: X) => X,
    functions: T
) {
    const mapped = {} as any;
    for (const key in functions) {
        mapped[key] = (...params) => fn(functions[key](...params));
    }
    return mapped as { [K in keyof T]: (...p: Parameters<T[K]>) => ReturnType<T[K]> };
}

Please read the original answer to get some explanation on the typings.

Related