How to check if a variable is an ES6 class declaration?

39.6k views Asked by At

I am exporting the following ES6 class from one module:

export class Thingy {
  hello() {
    console.log("A");
  }

  world() {
    console.log("B");
  }
}

And importing it from another module:

import {Thingy} from "thingy";

if (isClass(Thingy)) {
  // Do something...
}

How can I check whether a variable is a class? Not a class instance, but a class declaration?

In other words, how would I implement the isClass function in the example above?

11

There are 11 answers

9
loganfsmyth On BEST ANSWER

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?

7
Felix Kling On

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...

0
surfable On

I'm shocked lodash didnt have the answer. Check this out - Just like Domi I just came up with a solution to fix the glitches. I know its a lot of code but its the most working yet understandable thing I could produce by now. Maybe someone can optimize it by a regex-approach:

function isClass(asset) {

    const string_match = "function";

    const is_fn = !!(typeof asset === string_match);

    if(!is_fn){

        return false;

    }else{

        const has_constructor = is_fn && !!(asset.prototype && asset.prototype.constructor && asset.prototype.constructor === asset);

        const code = !asset.toString ? "" : asset.toString();

        if(has_constructor && !code.startsWith(string_match)){
            return true;
        }

        if(has_constructor && code.startsWith(string_match+"(")){
            return false;
        }

        const [keyword, name] = code.split(" ");

        if(name && name[0] && name[0].toLowerCase && name[0].toLowerCase() != name[0]){
            return true;
        }else{
            return false;
        }

    }

}

Just test it with:

console.log({
    _s:isClass(String),
    _a:isClass(Array),
    _o:isClass(Object),
    _c:isClass(class{}),
    fn:isClass(function(){}),
    fnn:isClass(function namedFunction(){}),
    fnc:isClass(()=>{}),
    n:isClass(null),
    o:isClass({}),
    a:isClass([]),
    s:isClass(""),
    n:isClass(2),
    u:isClass(undefined),
    b:isClass(false),
    pr:isClass(Promise),
    px:isClass(Proxy)
});

Just make sure all classes have a frist uppercased letter.

18
Syed On

Well going through some of the answers and thinks to @Joe Hildebrand for highlighting edge case thus following solution is updated to reflect most tried edge cases. Open to more identification where edge cases may rest.

Key insights: although we are getting into classes but just like pointers and references debate in JS does not confirm to all the qualities of other languages - JS as such does not have classes as we have in other language constructs.

some debate it is sugar coat syntax of function and some argue other wise. I believe classes are still a function underneath but not so much so as sugar coated but more so as something that could be put on steroids. Classes will do what functions can not do or didn't bother upgrading them to do.

So dealing with classes as function for the time being open up another Pandora box. Everything in JS is object and everything JS does not understand but is willing to go along with the developer is an object e.g.

  • Booleans can be objects (if defined with the new keyword)
  • Numbers can be objects (if defined with the new keyword)
  • Strings can be objects (if defined with the new keyword)
  • Dates are always objects
  • Maths are always objects
  • Regular expressions are always objects
  • Arrays are always objects
  • Functions are always objects
  • Objects are always objects

Then what the heck are Classes? Important Classes are a template for creating objects they are not object per say them self at this point. They become object when you create an instance of the class somewhere, that instance is considered an object. So with out freaking out we need to sift out

  • which type of object we are dealing with
  • Then we need to sift out its properties.
  • functions are always objects they will always have prototype and arguments property.
  • arrow function are actually sugar coat of old school function and have no concept of this or more the simple return context so no prototype or arguments even if you attempt at defining them.
  • classes are kind of blue print of possible function dont have arguments property but have prototypes. these prototypes become after the fact object upon instance.

So i have attempted to capture and log each iteration we check and result of course.

Hope this helps

'use strict';
var isclass,AA,AAA,BB,BBB,BBBB,DD,DDD,E,F;
isclass=function(a) {
if(/null|undefined/.test(a)) return false;
    let types = typeof a;
    let props = Object.getOwnPropertyNames(a);
    console.log(`type: ${types} props: ${props}`);


    return  ((!props.includes('arguments') && props.includes('prototype')));}
    
    class A{};
    class B{constructor(brand) {
    this.carname = brand;}};
    function C(){};
     function D(a){
     this.a = a;};
 AA = A;
 AAA = new A;
 BB = B;
 BBB = new B;
 BBBB = new B('cheking');
 DD = D;
 DDD = new D('cheking');
 E= (a) => a;
 
F=class {};
 
console.log('and A is class: '+isclass(A)+'\n'+'-------');
console.log('and AA as ref to A is class: '+isclass(AA)+'\n'+'-------');
console.log('and AAA instance of is class: '+isclass(AAA)+'\n'+'-------');
console.log('and B with implicit constructor is class: '+isclass(B)+'\n'+'-------');
console.log('and BB as ref to B is class: '+isclass(BB)+'\n'+'-------');
console.log('and BBB as instance of B is class: '+isclass(BBB)+'\n'+'-------');
console.log('and BBBB as instance of B is class: '+isclass(BBBB)+'\n'+'-------');
console.log('and C as function is class: '+isclass(C)+'\n'+'-------');
console.log('and D as function method is class: '+isclass(D)+'\n'+'-------');
console.log('and DD as ref to D is class: '+isclass(DD)+'\n'+'-------');
console.log('and DDD as instance of D is class: '+isclass(DDD)+'\n'+'-------');
console.log('and E as arrow function is class: '+isclass(E)+'\n'+'-------');
console.log('and F as variable class is class: '+isclass(F)+'\n'+'-------');
console.log('and isclass as variable  function is class: '+isclass(isclass)+'\n'+'-------');
console.log('and 4 as number is class: '+isclass(4)+'\n'+'-------');
console.log('and 4 as string is class: '+isclass('4')+'\n'+'-------');
console.log('and DOMI\'s string is class: '+isclass('class Im a class. Do you believe me?')+'\n'+'-------');

shorter cleaner function covering strict mode, es6 modules, null, undefined and what ever not property manipulation on object.

What I have found so far is since from above discussion classes are blue print not as such object on their own before the fact of instance. Thus running toString function would almost always produce class {} output not [object object] after the instance and so on. Once we know what is consistent then simply run regex test to see if result starts with word class.

"use strict"
let isclass = a =>{
return (!!a && /^class.*{}/.test(a.toString()))
}
class A {}
class HOO {}
let B=A;
let C=new A;
Object.defineProperty(HOO, 'arguments', {
  value: 42,
  writable: false
});


console.log(isclass(A));
console.log(isclass(B));
console.log(isclass(C));
console.log(isclass(HOO));
console.log(isclass());
console.log(isclass(null));
console.log(HOO.toString());
//proxiy discussion
console.log(Proxy.toString());
//HOO was class and returned true but if we proxify it has been converted to an object
HOO = new Proxy(HOO, {});
console.log(isclass(HOO));
console.log(HOO.toString());
console.log(isclass('class Im a class. Do you believe me?'));

Lead from DOMI's disucssion

class A {
static hello (){console.log('hello')}
hello () {console.log('hello there')}
}

A.hello();
B = new A;
B.hello();

console.log('it gets even more funnier it is properties and prototype mashing');  

class C {
  constructor() {
    this.hello = C.hello;
  }
static hello (){console.log('hello')}
}
C.say = ()=>{console.log('I said something')} 
C.prototype.shout = ()=>{console.log('I am shouting')} 

C.hello();
D = new C;
D.hello();
D.say();//would throw error as it is not prototype and is not passed with instance
C.say();//would not throw error since its property not prototype
C.shout();//would throw error as it is prototype and is passed with instance but is completly aloof from property of static 
D.shout();//would not throw error
console.log('its a whole new ball game ctaching these but gassumption is class will always have protoype to be termed as class');

2
lochiwei On

There are subtle differences between a function and a class, and we can take this advantage to distinguish between them, the following is my implementation:

// is "class" or "function"?
function isClass(obj) {

    // if not a function, return false.
    if (typeof obj !== 'function') return false;

    // ⭐ is a function, has a `prototype`, and can't be deleted!

    // ⭐ although a function's prototype is writable (can be reassigned),
    //   it's not configurable (can't update property flags), so it
    //   will remain writable.
    //
    // ⭐ a class's prototype is non-writable.
    //
    // Table: property flags of function/class prototype
    // ---------------------------------
    //   prototype  write  enum  config
    // ---------------------------------
    //   function     v      .      .
    //   class        .      .      .
    // ---------------------------------
    const descriptor = Object.getOwnPropertyDescriptor(obj, 'prototype');

    // ❗functions like `Promise.resolve` do have NO `prototype`.
    //   (I have no idea why this is happening, sorry.)
    if (!descriptor) return false;

    return !descriptor.writable;
}

Here are some test cases:

class A { }
function F(name) { this.name = name; }

isClass(F),                 // ❌ false
isClass(3),                 // ❌ false
isClass(Promise.resolve),   // ❌ false

isClass(A),                 // ✅ true
isClass(Object),            // ✅ true
3
Ian Carter On

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'
});

0
Andrea Giammarchi On

Such an old question and nearly no answer is correct except for this one and yet there is a caveat there ...

why wrong answers?

Let's start with anyone suggesting to invoke the function ... that's a disaster prone approach that should be removed from suggestions before ChatGPT would even consider that as code to suggest ... next ...

There are JS runtimes where the string representation of the function, or rest of the code, does not exist in production, maybe in debugging mode, but not necessarily in prod, quite the opposite.

This is because some JS runtime can save lot of final "bytecode" size by removing the source from pretty much all of its content.

Accordingly, everyone suggesting any string check doesn't know, or consider, these scenarios, but also any function toString method can be replaced with something else too, making most answers not bullet proof.

why no right answer?

The closest answer is the one checking writable at the prototype descriptor of any function:

  • shorthand methods such as {method(){}} won't have a prototype at all
  • transpiled classes to ES5 functions will likely have it writable unless the transpiler was very focused on this detail, like Babel, resulting in slightly slower runtime too
  • only native ES2015+ code that has not been transpiled will pass all tests: prototype exists and its writable value is exactly false
const isESClass = fn => (
  typeof fn === 'function' &&
  Object.getOwnPropertyDescriptor(
    fn,
    'prototype'
  )?.writable === false
);

It's important to understand this will fail in projects stuck into ES5 transpilation (for whatever reason) but there's fundamentally no way to guarantee a generic function, in the pre-ES2015 world, is a class or isn't, jQuery (among others) used patterns like the following and all cases are allowed:

function jQuery(...args) {
  if (!(this instanceof jQuery))
    return new jQuery(...args);
  // do everything jQuery does
}

Differently from ES2015+ classes, that utility works both as regular function and as new function, so basically there is no correct answer to this question, just a list of compromises and targets to consider.


For specs reader sake:

0
Domi On

This solution fixes two false positives with Felix's answer:

  1. It works with anonymous classes that don't have space before the class body:
    • isClass(class{}) // true
  2. It works with native classes:
    • isClass(Promise) // true
    • isClass(Proxy) // true
function isClass(value) {
  return typeof value === 'function' && (
    /^\s*class[^\w]+/.test(value.toString()) ||

    // 1. native classes don't have `class` in their name
    // 2. However, they are globals and start with a capital letter.
    (globalThis[value.name] === value && /^[A-Z]/.test(value.name))
  );
}

const A = class{};
class B {}
function f() {}

console.log(isClass(A));                // true
console.log(isClass(B));                // true
console.log(isClass(Promise));          // true

console.log(isClass(Promise.resolve));  // false
console.log(isClass(f));                // false

Shortcomings

Sadly, it still won't work with node built-in (and probably many other platform-specific) classes, e.g.:

const EventEmitter = require('events');
console.log(isClass(EventEmitter));  // `false`, but should be `true` :(
6
Mat On

What about:

function isClass(v) {
   return typeof v === 'function' && v.prototype.constructor === v;
}
0
João Sequeira On

Late to the party but another approach that would satisfy compilers and intent, if i understood correctly.

function isInheritable(t) {
    try {
        return Boolean(class extends t {
        })
    } catch {
        return false;
    }
}
3
aimadnet On

Maybe this can help

let is_class = (obj) => {
    try {
        new obj();
        return true;
    } catch(e) {
        return false;
    };
};