Search code examples
javascriptnode.jstypescriptclassinstanceof

Why does the `instanceof` operator return false on instance passed to library? (No inheritance involved)


I am building a library with TS. This library uses the ssh2 library as a dependency.

I'm trying to create a function that can either accept an ssh2 configuration object or an already existing Client instance to execute a command (this is a simplified case):

import { Client, ConnectConfig } from 'ssh2';

export function runCommand(params: Client | ConnectConfig) {
  if(params instanceof Client) {
    // do something
  } else {
    // create our own client
    // do something
  }
}

When I build this library and call the function like so:

const { readFileSync } = require("fs");
const { Client } = require("ssh2");
const { runCommand } = require("myLib");
const conn = new Client();

conn
  .on("ready", () => {
    console.log("Client :: ready");
    runCommand(conn);
  })
  .connect({
    host: "example.com",
    port: 22,
    username: "me",
    privateKey: readFileSync(process.env.HOME + "/.ssh/id_ed25519"),
  });

the instanceof check in my function will return false for some reason. What exactly is happening here? Is it a TS compilation issue or does Node.js see the Client from my lib's dependency and my consumer code's dependency as two different classes?

The compiled code looks something like:

const ssh2_1 = require("ssh2");
//...
function runCommand(params) {
    if (params instanceof ssh2_1.Client) {
        // do something
    }
    else {
       // create our own client
    // do something
    }
}

Solution

  • What may not be obvious from the behaviour of instanceof operator, is that the class must be exactly in the parent chain, in the sense of JavaScript object reference.

    Hence, if you import Client class twice, they are different actual object references, and instanceof may give a "false" negative.

    This is very probably what happens here: ssh2 is bundled with your library, hence it imports its own copy of the dependency. Whereas your app imports ssh2 on its own, leading to a separate copy.

    You have a few options:

    • Do not bundle the dependency with your library (e.g. with webpack externals), let the app install a single version that both your library and app will import; typical case of specifying peerDependencies
    • Do not use the instanceof operator; instead, use some heuristic to determine whether the object has the shape you need; typically check for some properties and their type
    • Re-export the dependency (e.g. Client class in your case) from your library, and use that one in your app (const { Client, runCommand } = require("myLib");), so that it is the exact same copy (since it is already bundled, why not re-using it instead of re-bundling it?)