Avoid silly inference for union types ( Element implicitly has an 'any' type because expression of type...)

Viewed 152

So I have a variant of question that was asked before but somehow non of the answers help. Here's the code:

type ModelA = {
    type: 'a'
    method: 'foo' | 'bar'
}

type ModelB = {
    type: 'b'
    method: 'baz'
}

type Model = ModelA | ModelB

const dictionary = {
    a: {
        foo: () => 'foo',
        bar: () => 'bar'
    },
    b: {
        baz: () => 'baz'
    }
}

function getMethod(m: Model): Function {
    return dictionary[m.type][m.method] <--- error
}

Element implicitly has an 'any' type because expression of type '"foo" | "bar" | "baz"' can't be used to index type '{ foo: () => string; bar: () => string; } | { baz: () => string; }'.
  Property 'foo' does not exist on type '{ foo: () => string; bar: () => string; } | { baz: () => string; }'.

If I use some silly type check like this:

function getMethod(m: Model) {
    if (m.type === 'a') {
        return dictionary[m.type][m.method]
    }
    return dictionary[m.type][m.method]
}

it works because that in both branches TS knows that it's safe to get the method.

I know this should be solved using generic but somehow I can't figure out.

1 Answers

In order to type it, you need to infer dictionary and each object property:

type ModelA = {
    type: 'a'
    method: 'foo' | 'bar'
}

type ModelB = {
    type: 'b'
    method: 'baz'
}

type Model = ModelA | ModelB

const dictionary = {
    a: {
        foo: () => 'foo',
        bar: () => 42
    },
    b: {
        baz: () => 'baz'
    }
}

const withDict = <
    Key extends string,
    SubKey extends string,
    Fn extends (...args: any[]) => any,
    Dict extends Record<Key, Record<SubKey, Fn>>
>(dict: Dict) =>
    <
        Type extends keyof Dict,
        Method extends keyof Dict[Type],
    >({ type, method }: { type: Type, method: Method }) =>
        dict[type][method]

const getMethod = withDict(dictionary)

// () => string
const result = getMethod({ type: 'a', method: 'foo' })

// () => number
const result2 = getMethod({ type: 'a', method: 'bar' })

// expected error
const result3 = getMethod({ type: 'a', method: 'z' })

Playground

As you might have noticed, I have used currying to infer dictionary.

In your case TypeScript was unable to infer dictiopnary since it was declared out of function scope.

You can find more examples of function arguments infering in my blog

UPDATE

You can also use this approach:

const withConfig = <Dictionary,>(config: Dictionary) =>
    <Type extends keyof Dictionary,
        Method extends keyof (Dictionary)[Type]
    >(
        type: Type,
        method: Method
    ) => config[type][method]

const applyConfig = withConfig({
    a: {
        foo: () => 'foo',
        bar: () => 42
    },
    b: {
        baz: () => 'baz'
    }
})

applyConfig('a', 'bar') // ok
applyConfig('a', 'baz') // expected error

Playground

Related