Wayne
Wayne

Reputation: 765

How to make a type check to prove that an unknown object has all of the nested keys I need it to have

I am trying to make sure the object has all of the keys I expect it to have (.result.items), the only problem is I get the following error:

Property result does not exist on type 'object'

on:

typeof (object.result as Record<string, any>).items === "object"

I do not understand this error because it seems that I already proved that object.result does exist with my previous line and it should be of type object.

let objectKeyCheck = (
  object: unknown
): object is { result: { items: object[] } } => {
  return (
    !!object &&
    typeof object === "object" &&
    !!(object as Record<string, any>).result &&
    typeof (object as Record<string, any>).result === "object" &&
    !!(object.result as Record<string, any>) &&
    typeof (object.result as Record<string, any>).items === "object"
  );
};

Upvotes: 1

Views: 621

Answers (1)

jcalz
jcalz

Reputation: 329773

The problem here is that when you use a type assertion on an expression, it "shields" the expression from any narrowing which might normally happen. For example:

function foo(x: string | number) {

  if (typeof x === "string") {
    console.log(x.toUpperCase()); // okay
  }

  if (typeof (x as string) === "string") {
    console.log(x.toUpperCase()); // error!
  }

}

In the first block we are performing a typeof type guard on x, which allows us to narrow it from string | number to just string. But in the second block we aren't guarding x, we're guarding x as string... which has no effect whatsoever on x.

Type assertions and narrowings don't really mix.


In order to fix this, I'd stay away from type assertions, and just use the support added in TypeScript 4.9 for unlisted property narrowing using the in operator:

let objectKeyCheck = (
    object: unknown
): object is { result: { items: object[] } } => {
    return (
        !!object &&
        typeof object === "object" &&
        "result" in object &&
        !!object.result &&
        typeof object.result === "object" &&
        "items" in object.result &&
        !!object.result.items &&
        typeof object.result.items === "object"
    );
};

Once we narrow object from unknown to object, then we check "result" in object and suddenly the compiler will treat object.result as a valid index, and the property type starts off as unknown. Then we can narrow object.result to type object, and do likewise with object.result.items.

Note that the above relies on a feature added in TypeScript 4.9. If you're using an earlier version there are almost certainly other approaches which also fix the problem, but I'm not going to digress here with exploring them as they are already out of date and will become less useful over time. The recommended thing to do is to upgrade TypeScript, and those who need code that targets a particular older version, might want to ask a new question with explicit requirements around language versioning.

Playground link to code

Upvotes: 1

Related Questions