Why doesn't TypeScript type guard 'in' narrows types to keyof types?
Asked Answered
S

1

9

Consider this code:

const obj = {
    a: 1,
    b: 2
}

let possibleKey: string = 'a'

if (possibleKey in obj) console.log(obj[possibleKey])

When possibleKey in obj is true, we know that possibleKey has type keyof typeof obj, right? Why doesn't TypeScript type system detects that and narrows down string to that type? Instead, it says:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ a: number; b: number; }'.
Spanishamerican answered 7/1, 2020 at 12:20 Comment(3)
"When possibleKey in obj is true, we know that possibleKey has type keyof typeof obj, right?" not exactly. possibleKey could be coming from the prototype of obj. Not sure if TS considers this an important distinction but it might, in which case possibleKey won't be a(n own) key of obj.Prairial
@Prairial - That's okay, TypeScript considers prototypes supertypes (effectively). The same problem occurs with a hasOwnProperty check.Dedans
@T.J.Crowder ah, good. I wasn't sure of that. It's likely a different problem, then.Prairial
B
8

Per the docs:

For a n in x expression, where n is a string literal or string literal type and x is a union type, the “true” branch narrows to types which have an optional or required property n, and the “false” branch narrows to types which have an optional or missing property n.

In other words, n in x narrows x, not n, and only for string literals or string literal types in union types. For that expression to work, you'd have to give the compiler more information, e.g. using a type assertion:

if (possibleKey in obj) {
  console.log(obj[<keyof typeof obj>possibleKey]);
}
Burble answered 7/1, 2020 at 12:27 Comment(6)
It's probably worth noting that obj[<keyof typeof obj>possibleKey] will have the type number in the OP's example because all properties of obj have number values, but if obj were (say) {a: 1, b: "foo"}, it would be number|string... So this gets you closer, and is useful in some places, but still may leave you with a further type check and assertion to do...Dedans
One stated reason not to narrow n is that the x object might have properties not mentioned in x's type (i.e., object types aren't exact). This doesn't apply when x's type is inferred from an object literal like {a:1, b:2} as in the question, though, but I guess they haven't wanted to special-case that. A user-defined type guard with signature <T extends object>(k: PropertyKey, o: T) => k is keyof T might help here, if this is a common operation.Pronunciamento
I think x is not narrowed either, because this also does not compile when noImplicitAny is active: const n = { a: 1 }; let x = 'a'; if (x in n) n[x] = 2Spanishamerican
@AndréValenti that's because in that example x is implicitly string, not the string literal type 'a'; if you make it let x: 'a' = 'a'; or const x = 'a'; the compiler is happier.Burble
Yeah, but, in that case, I see no point in asserting x in n, since it will always be true, except if someone messed with the object during runtime.Spanishamerican
@AndréValenti in that toy example, yes, but also e.g. let x: 'a' | 'b' | 'c'; x = 'a';. The point is x needs to have string literal type, or be a string literal.Burble

© 2022 - 2024 — McMap. All rights reserved.