Object Type Check By User Defined Type Guard with Record Type

eye-catchJavaScript/TypeScript

I wrote the following article before.

When we want to access a property of an object, we must somehow tell the compiler if the object implements the desired property. The article above explains how to do it with a user-defined type guard.

It is something like this below.

export function isPerson(object: unknown): object is Person {
    return Object.prototype.hasOwnProperty.call(object, "name")
        && Object.prototype.hasOwnProperty.call(object, "age");
}

However, it is cumbersome to write the long statement for many properties.

I will propose the different ways to solve the problem in this article.

Sponsored links

Unknown Object does not have any accessible properties

The problem above is that an argument is an unknown object. If it is an unknown object, the compiler doesn’t know which property the object has. Therefore, it shows an error if we access a property of the object.

const maybePerson: unknown = {
    name: "Yuto",
    age: 35,
};

// Object is of type 'unknown'
// maybePerson.name

Therefore, it is not possible to call hasOwnProperty directly.

maybePerson.hasOwnProperty("name");

That’s why we need to call it in the following way.

Object.prototype.hasOwnProperty.call(object, "name")

But we don’t want to write the code for all properties.

How does an Object look like

Then, we somehow need to tell the compiler if the object is an object. The easiest way to do it is to use any.

const maybePerson: unknown = {
    name: "Yuto",
    age: 35,
};

(maybePerson as any).hasOwnProperty("name")

But this is not nice. any is a dangerous type because it ignores all possible errors and tells the compiler that the object is valid in the context even if the code is not correct. It can cause a runtime error. If the object is any, the compiler doesn’t say anything for the error code that could be caught at compile time if the type is defined correctly.

Then, let’s consider which type can be a replacement for any data type. An object has string properties and the corresponding value that is an unknown type. There are two types to represent this structure as far as I know. The union type to combine the two types is the following.

export type UnknownObject = Record<string, unknown> | { [key: string]: unknown };

Both types take string property and unknown value. The following object can be both types.

const obj: UnknownObject = {
    prop1: "hello",
    prop2: "world",
    prop3: 123,
    func: () => 55,
};

Define user type guard strictly

Let’s define our own user type guard by using the definitions.

function isObject1(object: unknown): object is Record<string, unknown> {
    return typeof object === "object";
}

function isObject2(object: unknown): object is { [key: string]: unknown } {
    return typeof object === "object";
}

The result is the same for both. It’s up to your preference which one to use.

By using one of these functions, the object can be treated as an object.

function hasPersonProps(object: UnknownObject): boolean {
    return typeof object.name === "string" && typeof object.age === "number";
}

export function isPerson1(object: unknown): object is Person {
    if (!isObject1(object)) {
        return false;
    }
    // object is UnknownObject here
    return hasPersonProps(object);
}

export function isPerson2(object: unknown): object is Person {
    if (!isObject2(object)) {
        return false;
    }
    // object is UnknownObject here
    return hasPersonProps(object);
}

hasPersonProps checks not only if the object has desired properties but also the type of the properties.

In the article that is shown at the top of this article, it checks only the existence of the desired properties. So, this way is better than that because the properties are strictly checked.

Comments

Copied title and URL