Search code examples
typescriptcoercion

Why is Typescript coercing my keyof type to a never type and how do I fix it?


Sorry if this is a dupe, I'm new to TypeScript and am having trouble figuring out if similar-looking questions are related because a lot of them are doing very complex things. Anyway the problem is, I have a relatively simple setup that TS is choking on because it's coercing a type to never and I don't really understand why it's doing that. Here's the setup:

interface BigObject {
  foo: {
    a?: string
    b?: string
  }
  bar: {
    c?: string
    d?: string
  }
}

const instance: BigObject = {
  foo: {
    a: "a",
    b: "b",
  },
  bar: {
    c: "c",
    d: "d",
  }
}

function metafunction(bigObjProp: keyof BigObject) {
  type LittleObject = BigObject[typeof bigObjProp]
  // IDE hints show this ^^ as being correct, i.e. either of the two "sub interfaces"

  return function (littleObjProp: keyof LittleObject) { // <== littleObjProp is resolving to never
    return function (bigObject: BigObject) {
      const littleObject = bigObject[bigObjProp]
      return littleObject ? littleObject[littleObjProp] : "fallback value"
    }
  }
}

const firstClosure = metafunction("foo")
const secondClosure = firstClosure("a") // <== Argument of type "a" is not assignable to type "never"
const value = secondClosure(instance)

My expectation is that the value of value will be "a".

I don't understand why littleObjProp resolves to never. My assumption would be that because LittleObject is built from the type of the argument passed in to metafunction, TS would pick which "sub interface" to use for any given invocation. So, for example, when I call metafunction("foo"), TS would set LittleObject to { a?: string; b?: string } and thus, when I call firstClosure("a"), it would say, "ah yes, 'a' is indeed a valid key of LittleObject, carry on". It can't do this, however, because it always thinks that keyof LittleObject means never.

Can someone help me understand 1) why it's doing this and 2) how to accomplish what I'm trying to do? I realize it's a weird setup but I'm dealing with some weird React libraries and this is just where I am at the moment. Please assume I have to keep the same overall structure of a function returning a function returning a function as seen in the example. Also I would really appreciate it if you could keep your answer as simple as possible since I am a bit new to TS. Thanks in advance!


Solution

  • Make metafunction generic.

    As you have it above, there's no generic type. To be safe firstClosure is only going to take a key that foo and bar have in common, but they have no key in common so never is the only possible parameter. If you were to give them a key in common, firstClosure would be typed to accept that.

    interface BigObject {
      foo: {
        a?: string
        b?: string
        f?: string   // Added
      }
      bar: {
        c?: string
        d?: string
        f?: string   // Added
      }
    }
    
    const instance: BigObject = {
      foo: {
        a: "a",
        b: "b",
        f: "f",
      },
      bar: {
        c: "c",
        d: "d",
        f: "f",
      }
    }
    
    const secondClosure = firstClosure("f")   // "f" is the only valid value
    

    typescript playground

    By adding a generic, you can convince Typescript to keep the type information as a property of metafunction and firstClosure each, which gives your closure the type you're looking for.

    function metafunction<T extends keyof BigObject>(bigObjProp: T) {
      type LittleObject = BigObject[T]
    
      return function (littleObjProp: keyof LittleObject) {
        return function (bigObject: BigObject) {
          const littleObject = bigObject[bigObjProp]
          return littleObject[littleObjProp] ?? "fallback value"
        }
      }
    }
    

    typescript playground