Testing class compatibility with isAssignableFrom
Classes are a controversial topic in the Javascript/Typescript world. Are they a terrible idea? Are they really useful, when used sensibly? This post will make no attempt at answering either of these questions. This post is about a niche method that I first encountered many years ago in the world of Java - isAssignableFrom - and how you can go about writing it for use in Typescript/Javascript.
What does it do?
This is what the Javadocs have to say about it:
public boolean isAssignableFrom(Class<?> cls)
Determines if the class or interface represented by this Class object is either the same as, or is a superclass or superinterface of, the class or interface represented by the specified Class parameter. It returns true if so; otherwise it returns false.
So, given some class, you can use isAssignableFrom to figure out whether that class is a subclass of some other class.
Why would I need this?
If you're thinking it's a bit niche, yes, I agree. isAssignableFrom is one of those methods you don't use much. But when you need it, you need it.
JsPlumb offers the concept of Decorators - a class that can add arbitrary extra content to the UI after a layout has been run. These can be used for a great variety of things, and if you're a JsPlumb user but haven't yet come across them, we'd encourage you to check them out.
You can register a decorator on JsPlumb using one of two different syntaxes - you can create an extension of Decorator and register it:
import { Decorator, Decorators, DecorateParams, DecorateResetParams } from "@jsplumbtoolkit/browser-ui"
class MyDecorator extends Decorator {
decorate(params: DecorateParams): void {
...
}
reset(params: DecorateResetParams): void {
}
}
Decorators.register("MY_DECORATOR", MyDecorator)
...or you can just register an function whose shape fits the bill (specifically, it matches the IDecorator interface):
import { Decorators, DecorateParams, DecorateResetParams } from "@jsplumbtoolkit/browser-ui"
Decorators.register("MY_DECORATOR", function() {
this.decorate = (params: DecorateParams): void => {
...
}
this.reset = (params: DecorateResetParams): void => {
}
})
The register method has this signature:
register:(name:string, dec:Constructable<Decorator>)
Constructable<Decorator> means "a function which, when invoked with new..., will return an instance of Decorator.". We use this interface in a few places in the JsPlumb code.
export type Constructable<T> = { new(...args: any[]): T }
So the question is, how does register know what was passed in? Was it given a Decorator, or was it given some other function? isAssignableFrom to the rescue!
const decoratorMap:Record<string, Constructable<Decorator>> = {}
register:(name:string, dec:Constructable<Decorator>) => {
if (isAssignableFrom(dec, Decorator)) {
decoratorMap[name] = dec
} else {
decoratorMap[name] = functionalDecorator(dec)
}
}
The register method determines whether or not it's been given a Decorator and just stashes it if so. Otherwise it uses the helper method functionalDecorator to wrap what it was given (the details of that method are orthogonal to this discussion).
How does it work then?
Show Me The Code
It's a simple method:
export function isAssignableFrom(object:any, cls:any) {
let proto = object.prototype
while (proto != null) {
if (proto instanceof cls) {
return true
}
proto = proto.prototype
}
return false
}
It basically walks the prototype chain and tests each prototype via an instanceof test.
Isn't this just instanceof?
No! It may seem like it is, but the key here is that comparison is being made on a class, not on an object. We want to know when someone has passed a reference to a class into register.
Consider this console session:
class Foo {}
class Bar extends Foo {}
const bar = new Bar()
bar instanceof Bar
// --> true
bar instanceof Foo
// --> true
So far, so good: our instance of Bar, which is a subclass of Foo, is recognised as being both an instance of Foo and of Bar. But how about this:
Bar instanceof Foo
// --> false
which makes sense really, because Bar is not an instance of anything - it's a class.
If we want to check whether we can consider Bar as Foo we're going to need our isAssignableFrom function:
isAssignableFrom(Bar, Foo)
// --> true
Start a free trial
Get in touch!
If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.