How to check if an object implements an interface in Typescript

eye-catch JavaScript/TypeScript

When using Typescript the compiler detects the type error. It supports any data type but it is not recommended to use because it tells the compiler that it doesn’t have to check the data type of the object. When the object isn’t the defined data type it crashes at runtime. It’s better to define concrete data type as much as possible. If it’s not possible to define concrete possible data types in the function you should use unknown data type instead of any.

You can clone my git repository if you want to run it on your pc.

https://github.com/yuto-yuto/BlogPost/tree/master/src/simple-code/interface-type
Sponsored links

Check the object data type by Type Guards

“in” Type Guards

Type Guard can be used when the object is Object type but if it is unknown data type it might not be an object. Therefore, the compiler says object is of type 'unknown'. In this case, you need to cast it to any.

const personData: unknown = {
    name: "yuto",
    age: 30,
};

if ("name" in (personData as any)) {
    console.log((personData as any).name);
}

However, it doesn’t make sense to use unknown in this case and eslint shows a warning when any is used. Another way that we may come up with is a combination of instanceof and in keyword but it doesn’t work because the compiler still doesn’t know what data type personData is and it shows an error.

if (personData instanceof Object && "name" in personData) {
    // Property 'name' does not exist on type 'never'
    console.log(personData.name);
}

instanceof interface

I know you tried to use instanceof for interface but the compiler shows an error like this below.

if(personData instanceof Person) {}
// 'Person' only refers to a type, but is being used as a value here.ts(2693)

As the error message says interface is a type. instanceof requires an instance but an interface is just a definition.

User defined Type Guards

User-defined type guards can solve the problem.

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

The point here is to call hasOwnProperty function via call function because we don’t know whether the object argument is object type or not.

Failed trial by Abstract class

The following code is another trial but it didn’t work because data is just an object. It’s not a class.

export abstract class ManyArgs {
    public arg1: string = "";
    public arg2: string = "";
    public arg3: string = "";
}

console.log("---- Abstract ----")
const data = {
    args1: "str 1",
    args2: "str 2",
    args3: "str 3",
};
if (data instanceof ManyArgs) {
    console.log(`${data.arg1}, ${data.arg2}, ${data.arg3}`)
} else {
    console.log("instanceof doesn't work.")
}

// result
// ---- Abstract ----
// instanceof doesn't work.

I tried to create a generic function for the check but it was impossible to create it because I couldn’t find a way to get a property list from interface. Object.keys() function requires object data type and we cannot pass interface there.

Sponsored links

Check if the variable is NodeJS.ErrnoException type

Let’s check an actual example. fs.promises.readFile function throws NodeJS.ErrnoException. The data type of error variable used in catch is unknown from TypeScript version 4.4. The source is here. We can turn off useUnknownInCatchVariables or add as any to cast it but let’s try to use a user-defined type guard here.

The type guard function looks like this. There are other properties available but I think these two are enough.

export function isErrnoException(object: unknown): object is NodeJS.ErrnoException {
    return Object.prototype.hasOwnProperty.call(object, "code")
        || Object.prototype.hasOwnProperty.call(object, "errno");
}

Let’s try to read a file. If the file doesn’t exist, it throws an error. Without this type check, the compiler shows errors because those properties don’t exist on type “unknown”.

async function runExample() {
    try {
        await fs.promises.readFile("/not-exist-file");
    } catch (e) {
        if (isErrnoException(e)) {
            console.log(`e.code: ${e.code}`);
            console.log(`e.errno: ${e.errno}`);
            console.log(`e.message: ${e.message}`);
            console.log(`e.name: ${e.name}`);
            console.log(`e.path: ${e.path}`);
            console.log(`e.stack: ${e.stack}`);
            console.log(`e.syscall: ${e.syscall}`);
        } else {
            console.log(e);
        }
    }
}
runExample()
    .then(() => console.log("done"))
    .catch(() => console.log("error"));

// e.code: ENOENT
// e.errno: -4058
// e.message: ENOENT: no such file or directory, open 'C:\not-exist-file'
// e.name: Error
// e.path: C:\not-exist-file
// e.stack: Error: ENOENT: no such file or directory, open 'C:\not-exist-file'
// e.syscall: open

Strict Object Type Check

The way above checks only the property’s existence. If two interfaces have the same properties but one of the data types is different, we somehow need to differentiate them. In this case, typeof needs to be used to check the data type. However, if the data type is unknown type, we can’t access the property. The following post shows how to solve the problem.

Conclusion

If an interface has a lot of properties it may be cumbersome to create a check function but there is no workaround. Let’s consider a better structure in this case. We may be able to separate the interface if it has many properties.
If you know better solutions, please leave comments.

Do you want to learn more? The following posts may be helpful.

Comments

  1. asdf says:

    Why would you write an article to explain in detail how to incorrectly do something??

    • Yuto Yuto says:

      Do you mean “Failed trial by Abstract class”?
      Because someone might try to do a similar thing. If I don’t like the popular solution that we can easily find in StackOverflow or somewhere else, I try to do different things.
      If it doesn’t work but looks correct, it takes me a while to know it can’t work because those trials are generally not written.
      This failed trial could save time.
      A beginner could also learn a new thing and the difference between the correct one and the incorrect one.

    • Chrsto says:

      Don’t be rude.
      Typescript interface are just bad, it’s not his fault.
      They should do something about it.

  2. Oleksandr says:


    // ------
    // strong implementation
    // ------
    interface IPersonData {
    name: string;
    age: number;
    }

    const personData: IPersonData = {
    name: "yuto",
    age: 30
    };

    let tmp: IPersonData = personData;
    // ------
    // extended interface example
    // ------
    interface IPersonData {
    name: string;
    age: number;
    }

    class PersonData implements IPersonData {
    age: number;
    name: string;
    gender: string; // additional field
    }

    const personData: PersonData = {
    name: "yuto",
    age: 30,
    gender: 'MAN'
    };

    let tmp: IPersonData = personData;
    // ------
    // You can also define and use your check method

    function checkImplements(x: T) { }
    // ...
    checkImplements(personData);

Copied title and URL