Search code examples
typescriptmapped-types

Create object of mapped type with a constraint


Similar to this question I'm trying to create an object of a mapped type. However, I keep getting type errors in the implementation.

Here's a toy example:

type SingleArgFunction<A> = (x: A) => A;

type ArrayedReturnFunction<A> = (x: A) => A[];

// create an arrayed function from a single arg function

type MakeArrayed<F> = F extends SingleArgFunction<infer A> ? ArrayedReturnFunction<A> : never;

// mapped type

type MakeArrayedAll<FS> = {
    [K in keyof FS]: MakeArrayed<FS[K]>
}

// example usage

type MyFunctions = {
    foo: SingleArgFunction<number>,
    bar: SingleArgFunction<string>
}

type MyArrayedFunctions = MakeArrayedAll<MyFunctions> // ok

// functions to convert an object of single arg functions to object of arrayed functions

interface SingleArgFunctionObject {
    [key: string]: SingleArgFunction<any>
}

function makeArrayed<A>(f: SingleArgFunction<A>): ArrayedReturnFunction<A> {
    return function (x) {
        return [f(x)];
    }
}

function makeArrayedAll<FS extends SingleArgFunctionObject>(fs: FS): MakeArrayedAll<FS> {
    const keys = Object.keys(fs) as (keyof FS)[];
    const result = {} as MakeArrayedAll<FS>;

    for (let key of keys) {
        result[key] = makeArrayed(fs[key]);
    }

    return result;
}

This gives the following type error for result:

const result: MakeArrayedAll<FS>
Type 'ArrayedReturnFunction<any>' is not assignable to type 'MakeArrayed<FS[keyof FS]>'.(2322)

The difference with the previous question is that the properties of the unmapped type FS need to be constrained via FS extends SingleArgFunctionObject. Is there a solution other than asserting result as any?

Playground


Solution

  • I'd be inclined to refactor your types so as to avoid conditional types (which are hard for the compiler to reason about) and instead use mapped types and inference from mapped types as much as possible. Your makeArrayed() function looks fine to me as-is.

    I'll define SingleArgFunctionObject and ArrayedReturnFunctionObject to be generic in the object type AS that it maps over... the idea is that each property of AS is treated like the A in your SingleArgFunction<A> and ArrayedReturnFunction<A>:

    type SingleArgFunctionObject<AS extends object> = {
        [K in keyof AS]: SingleArgFunction<AS[K]>
    }
    
    type ArrayedReturnFunctionObject<AS extends object> = {
        [K in keyof AS]: ArrayedReturnFunction<AS[K]>
    }
    

    Armed with this, makeArrayedAll() will be generic in AS. The tricky bit for getting the compiler not to complain is to make the for loop treat each key as a generic K extends keyof AS. That's easier to do with the forEach() method of arrays:

    function makeArrayedAll<AS extends object>(
        fs: SingleArgFunctionObject<AS>
    ): ArrayedReturnFunctionObject<AS> {
        const result = {} as ArrayedReturnFunctionObject<AS>;
        (Object.keys(fs) as (keyof AS)[]).forEach(<K extends keyof AS>(key: K) => {
            result[key] = makeArrayed(fs[key]);
        })
        return result;
    }
    

    That should produce the same result as your code, but now the compiler is more confident that makeArrayed(fs[key]) of type ArrayedReturnFunction<AS[K]> is assignable to result[key] of type ArrayedReturnFunctionObject<AS>[K].

    Okay, hope that helps; good luck!

    Playground link