7

tl;dr why does

const func = (a: unknown) => {
  if (a && typeof a === 'object' && 'b' in a) {
    a.b;
  }
};

Give the following error message

Property 'b' does not exist on type 'object'.

?

Edit: After looking into this more closely I have an even more minimal example. So let me rephrase my question:

How to probably narrow object type in TypeScript?

tl;dr why does

const func = (a: object) => {
  if ('b' in a) {
    a.b;
  }

give the following error message:

Property 'b' does not exist on type 'object'.

?

Max Coplan
  • 1,111
  • 13
  • 27
  • see https://stackoverflow.com/questions/51439843/unknown-vs-any – novarx Jan 14 '21 at 21:14
  • 1
    @novarx thank you. I think I have a pretty good understanding of the difference between `any` and `unknown`. If this were `a: any` this wouldn't even be a question. However, `unknown` is a good default, instead of `any` – Max Coplan Jan 14 '21 at 21:17
  • maybe this will be of help https://www.typescriptlang.org/docs/handbook/advanced-types.html – D Pro Jan 14 '21 at 21:39
  • regarding your edit. Don't use the object type in typescript. a better reference would be `Record` which would describe an object with any keys with an unknown type on the value of this key. If you do this typescript can narrow down the correct property. Have a look at [this playground](https://www.typescriptlang.org/play?#code/MYewdgzgLgBAZgVzMGBeGAKAhgLhgJQFNQAnAEwB5oSBLMAcwBoYkBrMEAdzAD4BKNDxgBvALAAoGDBpxMAcgBGc6WBhYBYyVLUA6BQG4JUgL4TjQA) – DivisionByZero Jan 15 '21 at 17:45
  • That makes sense. But the reason I want this is because of the initial question. Is there a way I can narrow `unknown` to `Record`? – Max Coplan Jan 15 '21 at 21:27

1 Answers1

7

You should have a closer look at custom type guard. They basically let the compiler know, that if the condition passes, the checked value will have a specific type.

In your case:

  1. Define a custom type guard
const hasB = (value: unknown): value is { b: unknown } => {
  return (
    typeof value === 'object'
    && value !== null
    && 'b' in value
  );
}
  1. Check your value with it
const func = (a: unknown) => {
  if (hasB(a)) {
    a.b;
  }
};

Playgound example

DivisionByZero
  • 148
  • 2
  • 6
  • 2
    Thank you. I guess I knew this was possible too, but I don't understand why it's necessary in this circumstance. The compiler should be able to infer `{b: unknown}` without having to use a custom type guard. Is this a known limitation/future feature of TypeScript, or is there a reason the type cannot be narrowed normally? – Max Coplan Jan 15 '21 at 16:05
  • 1
    Look at the first example of [advanced types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types), they have this line saying `if ("swim" in pet) pet.swim();`, where the type of `pet` outside this block is `Fish | Bird`. If you think of `unknown` as the intersection of all the types (which I believe is an accurate way to think about it), then why does a similar logic not apply here? – Max Coplan Jan 15 '21 at 16:10
  • 1
    I have updated my question to be more specific – Max Coplan Jan 15 '21 at 16:17
  • 1
    don't think about `unknown` as an intersect of all types. That would be `any`. `unknown` is the opposite. until proven otherwise it's none of all possible types. – DivisionByZero Jan 15 '21 at 17:39
  • 1
    Wouldn't `any` be the union of all possible types? – Max Coplan Jan 15 '21 at 21:26