How to check if a variable is an ES6 class declaration?
Checking the prototype
and its writability should allow to determine the type of function without stringifying, calling or instantiating the input.
/**
* determine if a variable is a class definition or function (and what kind)
* @revised
*/
function isFunction(x) {
return typeof x === 'function'
? x.prototype
? Object.getOwnPropertyDescriptor(x, 'prototype').writable
? 'function'
: 'class'
: x.constructor.name === 'AsyncFunction'
? 'async'
: 'arrow'
: '';
}
console.log({
string: isFunction('foo'), // => ''
null: isFunction(null), // => ''
class: isFunction(class C {}), // => 'class'
function: isFunction(function f() {}), // => 'function'
arrow: isFunction(() => {}), // => 'arrow'
async: isFunction(async function () {}) // => 'async'
});
If you want to ensure that the value is not only a function, but really a constructor function for a class, you can convert the function to a string and inspect its representation. The spec dictates the string representation of a class constructor.
function isClass(v) {
return typeof v === 'function' && /^\s*class\s+/.test(v.toString());
}
Another solution would be to try to call the value as a normal function. Class constructors are not callable as normal functions, but error messages probably vary between browsers:
function isClass(v) {
if (typeof v !== 'function') {
return false;
}
try {
v();
return false;
} catch(error) {
if (/^Class constructor/.test(error.message)) {
return true;
}
return false;
}
}
The disadvantage is that invoking the function can have all kinds of unknown side effects...
I'll make it clear up front here, any arbitrary function can be a constructor. If you are distinguishing between "class" and "function", you are making poor API design choices. If you assume something must be a class
for instance, no-one using Babel or Typescript will be be detected as a class
because their code will have been converted to a function instead. It means you are mandating that anyone using your codebase must be running in an ES6 environment in general, so your code will be unusable on older environments.
Your options here are limited to implementation-defined behavior. In ES6, once code is parsed and the syntax is processed, there isn't much class-specific behavior left. All you have is a constructor function. Your best choice is to do
if (typeof Thingy === 'function'){
// It's a function, so it definitely can't be an instance.
} else {
// It could be anything other than a constructor
}
and if someone needs to do a non-constructor function, expose a separate API for that.
Obviously that is not the answer you are looking for, but it's important to make that clear.
As the other answer here mentions, you do have an option because .toString()
on functions is required to return a class declaration, e.g.
class Foo {}
Foo.toString() === "class Foo {}" // true
The key thing, however, is that that only applies if it can. It is 100% spec compliant for an implementation to have
class Foo{}
Foo.toString() === "throw SyntaxError();"
No browsers currently do that, but there are several embedded systems that focus on JS programming for instance, and to preserve memory for your program itself, they discard the source code once it has been parsed, meaning they will have no source code to return from .toString()
and that is allowed.
Similarly, by using .toString()
you are making assumptions about both future-proofing, and general API design. Say you do
const isClass = fn => /^\s*class/.test(fn.toString());
because this relies on string representations, it could easily break.
Take decorators for example:
@decorator class Foo {}
Foo.toString() == ???
Does the .toString()
of this include the decorator? What if the decorator itself returns a function
instead of a class?