How to infer type at key from types at other keys via function signature?

Viewed 240

Typescript Playground

I would like to get types inferred for the init key in my BigObj objects created by a function. I'd like to provide the nums and strs keys and have the init signature have it's arguments and return type determined by nums and strs.

type NumMap = { [key: string]: number };
type StrMap = { [key: string]: string };
type UnknownMap<T> = {[k in keyof T]: unknown}

type InitFn<T1 extends NumMap, T2 extends StrMap> =
    (from: UnknownMap<T1>) => UnknownMap<T2>;

type BigObj<N extends NumMap, S extends StrMap> = {
    nums: N;
    strs: S;
    init: InitFn<N,S>
}

function makeBigObj<B extends BigObj<NumMap, StrMap>>(
    opts: Partial<B>
): B {
    return {
        nums: {} as NumMap,
        strs: {} as StrMap,
        init: (() => ({})) as InitFn<NumMap, StrMap>,
        ...opts
    } as B;
}

But the init signature is not inferred properly:

makeBigObj({
    nums: { a: 1 },
    strs: { b: "a" },
    init: function(from) {
        from.a; // should be inferred to be present
        from.x; // should produce error
        return {
            b: [], // should be inferred to be required
            y: []  // should produce error
        }
    }
})

I thought because I control the makeBigObj function it may infer the types better if I wrote it like this:

function makeBigObj2<B extends BigObj<NumMap, StrMap>>(
    opts: Partial<Omit<B, "init">>,
    initFn: B["init"]
): B {
    return {
        nums: {} as NumMap,
        strs: {} as StrMap,
        init: initFn,
        ...opts
    } as B;
}

But the result is the same:

makeBigObj2({
    nums: { a: 1 },
    strs: { b: "a" }
}, function(from) {
    from.a; // should be inferred to be present
    from.x; // should produce error
    return {
        b: [], // should be inferred to be required
        y: []  // should produce error
    }
})

How do I get init to be inferred to have the a and b keys and not the x and y ones?

1 Answers

I found two issues which cause lack of inference.

First, the B extends BigObj<NumMap, StrMap> generic is problematic because specifying NumMap/StrMap directly will simply use those types directly, resulting in number/string instead of inferred keys. Instead, each Map should be its own generic so that each one can be inferred independently.

function makeBigObj<N extends NumMap, S extends StrMap, B extends BigObj<N, S>>(

Secondly, for some reason having the parameter opts: Partial<B> refer to the B generic causes the compiler to expand its type to BigObj<NumMap, StrMap>. This can be worked around by not using the generic for the parameter but rather referring to BigObj directly:

function makeBigObj<N extends NumMap, S extends StrMap>(
    opts: Partial<BigObj<N, S>>
): BigObj<N, S> {
    return {
        nums: {} as NumMap,
        strs: {} as StrMap,
        init: (() => ({})) as InitFn<NumMap, StrMap>,
        ...opts
    } as BigObj<N, S>;
}

This correctly makes the line with from.x produce an error. The return type for init is also correctly inferred, but the compiler doesn't produce any errors for it. I'd guess this is because Typescript has no exact object types.

Related