简体   繁体   中英

How to narrow object type with “in” and/or “hasOwnProperty”

I'm writing a type guard for an interface, and I'm noticing that I cannot narrow object with in or Object.hasOwnProperty.call or arg.hasOwnProperty . Like this:

interface Test {
    quest: string;
}

function isTest(arg: unknown): arg is Test {
    // this successfully narrows arg to "object"
    if (typeof(arg) !== "object" || arg === null) {
        return false;
    }

    if (!Object.hasOwnProperty.call(arg, "quest")) {
        return false;
    }
    // at this point, I still cannot access arg.quest, because
    // "Property 'quest' does not exist on type 'object'". The same thing happens
    // if I invert the check and try to access it inside the conditional.

    if (!arg.hasOwnProperty("quest")) {
        return false;
    }
    // same problem, cannot access arg.quest because arg is still just "object"

    if (!("quest" in arg)) {
        return false;
    }
    // still the same problem.

    return true;
}

Because of this, I'm having to do a lot of type casting, which is annoying because the actual interface I want to check has many properties, not all of which are basic primitives. So basically, for each property I have to make something that looks like this:

function isTest(arg: unknown): arg is Test {
    if (typeof(arg) !== "object" || arg === null) {
        return false;
    }

    if (!Object.hasOwnProperty.call(arg, "quest")) {
        return false;
    }
    if (typeof((arg as {quest: unknown}).quest) !== "string") {
        return false;
    }

    return true;
}

... and generally, I feel like if I have to use as I've done something wrong. So how do I narrow object to an interface without writing a separate type guard for each property of the interface?

As to why I need such robust guards, it's because this is going in a server and I'm checking payloads sent to its API after being JSON.parse d so that I can do complex operations with them without worrying about finding out halfway through that the data is poorly structured.

Although any is generally unsafe, I find it to be useful as the type of a parameter to a user-defined type guard function :

function isTest(arg: any): arg is Test {
  return (typeof arg === "object") && (arg !== null) &&
    ("quest" in arg) && (typeof arg.quest === "string");
}

The above compiles without error. No, it is not actually enforcing safety inside the implementation, since any effectively shuts off type checking. But as long as you verify that the implementation is doing the right thing, using any makes the compiler get out of your way so that the callers of isTest() can have type safety.

This is essentially the same thing as using unknown and type assertions , though, which you've said makes you feel like you've done something wrong.

But keep in mind that the implementations of user-defined type guard functions work this way no matter what. The only thing the compiler verifies for you is that your return type is boolean . It just believes you when you say that such a boolean result can be treated as the type guard represented by the type predicate arg is Test . You could write this, for instance:

function isBadTest(arg: unknown): arg is Test {
  return Math.random() < 0.5; // no error
}

So while it makes sense that you'd like to get as much type safety as possible from the compiler, you should remember that implementing user-defined type guards shifts some of that burden onto you. Adding an as or an any to the mix doesn't really give up much.


That being said, let me ignore the fact that you're writing your own type guard function and only pay attention to the issue where you have a value of type unknown and you're trying to get the compiler to see if it can be narrowed to Test , or at least to something with a quest property you can access.

The problem here is that TypeScript just doesn't have a built-in type guard that adds properties to the known type of an object type. As you noted, you can't currently use in to do it (see microsoft/TypeScript#21732 ), and you can't use hasOwnProperty() to do it (see microsoft/TypeScript#18282 ). If you want such a type guard, you have to write your own. For example:

function hasProp<T extends object, K extends PropertyKey>(
  obj: T, 
  key: K
): obj is T & Record<K, any> {
  return key in obj;
}

function isTest2(arg: unknown): arg is Test {
  if (typeof arg !== "object") return false;
  if (arg === null) return false;
  if (!hasProp(arg, "quest")) return false; // use it here,
  if (typeof arg.quest !== "string") return false; // no error here
  return true;
}

Once you start going this route, though, and keeping in mind that your ultimate intent is to validate deserialized objects at runtime to see that they conform to TypeScript interfaces, I end up concluding that you'll want something like this answer . In there I wrote a mini schema-validator that builds complex type guards from simpler ones. Using that code, your isTest() is just:

const isTest3: G.Guard<Test> = G.gObject({ quest: G.gString });

where G.gObject() takes an object of property type guards and produces a type guard for an object with those properties, and G.gString is a type guard which verifies that its argument is a string .

So you might want to check out the other answer to avoid having to reimplement type guards for each interface.

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM