Stack Exchange
Index type check failing after explicit `key in obj` condition
The snippet below snippet is throwing a type error. I expect label to be of type 'a' | 'b', but it is string.
export const obj = {
  a: 1,
  b: 2
} as const

const label = Math.random().toString(32)

if (label in obj) {
  const p = obj[label];
  console.log(p)
}

Element implicitly has an `any` type because expression of type `string` cannot be used to index type `{ readonly a: 1; readonly b: 2; }`.
No index signature with a parameter of type `string` was found on type `{ readonly a: 1; readonly b: 2; }`.(7053)


ref: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-in-operator-narrowing

The return type of toString is a string, so label will be defined as string, because you initialize it at this position. You can tell typescript that it is an other type, but this will result in an error in this line.
What you can do is to use a function with return true or false and return a is type, but I think you need a type for your object.
Here (ts playground) is an example implementation
interface AB {
    a: number,
    b: number,
}

export const obj: AB = {
  a: 1,
  b: 2
} as const

function labelIsInObj(label: string, obj: AB): label is keyof AB {
    return label in obj;
}

let label = Math.random().toString(32)

if (labelIsInObj(label, obj)) {
  const p = obj[label];
  console.log(p)
}

If you create an Object from a class you can also use the Class as a type

The same way I expect a string | null type to become string after a != null I expect that type to be narrowed down to keyof typeof obj after label in obj

yeah, but here typescript is not perfect, but they getting better from day to day. As an alternative you can also cast label like: const p = obj[label as keyof typeof obj];

there is also a taturial where you have a playground with tips to train something like this. It helped me a lot for the first steps but I am not able to find it anymore.

Found the (sad) answer here. This is a TypeScript limitation and can not be done.
After some fiddling I found a replacement for the in operator to use as type predicate. The workaround below, narrows the type of label to the keys present in T.
Extract<keyof T, string> is necessary when label is string because TypeScript is explicitly returning "string | number" as type for a keyof of object. number can't be assign to a string. Makes you wonder why the inconsistency.
From the docs:

The keyof type operator
... this is because JavaScript object keys are always coerced to a string, so obj[0] is always the same as obj["0"].

TS Playground ⚽️
A way around this is to widen the type for k to string | number | symbol so that it matches keyof.
const hasKey = <T extends object>(o: T, k: string | number | symbol): k is keyof T => {
  return k in o
}

const hasStringKey = <T extends object>(o: T, k: string): k is Extract<keyof T, string> => {
  return k in o
}

export const obj1 = {
  a: 1,
  b: 2
} as const

export const obj2 = {
  c: 1,
  d: 2,
  e: 'e',
  f: 'f'
} as const

const label = Math.random().toString(32)

if (hasKey(obj1, label)) {
  const p = obj1[label];  // label: "a" | "b"
  console.log(p)
}

if (hasKey(obj2, label)) {
  const p = obj2[label]; // label: "c" | "d" | "e" | "f"
  console.log(p)
}

if (hasStringKey(obj1, label)) {
  const p = obj1[label];  // label: "a" | "b"
  console.log(p)
}

if (hasStringKey(obj2, label)) {
  const p = obj2[label]; // label: "c" | "d" | "e" | "f"
  console.log(p)
}


Is not so much different to my solution, ok you save the interface but you have to define one type more…

@CordlessWool I updated mine with more info around why your solution is not exhaustive and a better example. As matter of fact, my old example was broken and had label: never as type narrowing.
