46

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?

2

12 Answers 12

45

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

Sign up to request clarification or add additional context in comments.

7 Comments

The second part of the statement will fail on some implementations (as the spec isn't very specific on that part). E.g. transpiled ES6 in Babel builds, however in this case you can still check if class exists and you can tack on || /_class\S+/i.test(v.toString()) to the statement for that.
Interestingly, it seems that in the Node.js REPL, you can distinguish between classes and function ƒ () {} because ƒ will have an arguments property, while a class doesn't. () => {} will not have prototype, unlike a class or function. However, when running script files with Node this doesn't seem to hold…
Don't call the function to test it! What if it has side effects? What if the function is question is fireNuclearWeapons?
FYI class\s+ can fail for minified classes like var foo=class{};.
@wprl the node REPL is not in strict mode. In strict mode, there's no difference.
|
21

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?

9 Comments

There is a class. Regular ES5 constructor functions and ES 6 class constructor functions are not the same. Just try Thingy() to see the difference. ES6 class constructor functions will throw an error because they may not be invoked without new. This answer is helpful but it does not answer the original question and should imho not have been accepted.
I think that's an inaccurate distinction. Certain functions cannot be used with new (like arrow functions) and certain functions cannot be used without new (like classes). That doesn't make them any more or less a function. Using ES6 class syntax is one way to create a function that cannot be created without new, but there are others too. Trying to differentiate between subtypes of functions is pretty much always going to be a bad idea because you'll never get it right 100% of the time.
Let me put it this way... I didn't come to this page to learn about typeof x == 'function'... The answer is very unsatisfying imho.
I'm just going to note that the the regex tests false with the \s before the class
@KnightYoshi Good catch, fixed.
|
17

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

3 Comments

This returns false in my tests: ( function funk (x){ } ) . hasOwnProperty ('arguments'); . So it does not tell me that 'funk' is a plain function as opposed to a class. This happens on Node.js
@PanuLogic - added a fix based on your comment and reading bugzilla.mozilla.org/show_bug.cgi?id=1606421 the arguments property seems to be implementation-specific (allthough it works on node>8-18 and chrome) - so by replacing the check for arguments by inspecting the writability of the prototype descriptor allows the differentiation between function and class.
Good to know about the 'writability' -difference. Do you think it is based on the standard, or is it just the way it happens to be with current browser-versions?
7

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:

Comments

6

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

2 Comments

Now I'm curious if property flags in class/function prototype is described on latest ECMA spec 🤔
"(I have no idea why this is happening, sorry.)" ... every method shorthand/cut version has no prototype ... see {method(){}} and inspect that object method descriptor to realize there's no prototype. This answer kinda nails it except it could use a WeakMap to avoid checking same classes or functions over and over
1

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

Comments

0

What about:

function isClass(v) {
   return typeof v === 'function' && v.prototype.constructor === v;
}

6 Comments

this answer is WRONG! (function(){}).constructor - this returns a function
(function(){}).constructor === window.Function is true, which is what you would expect: a function is an instance of Function. Note that I compare with the value's prototype constructor and not the value's constructor.
What i meant is that the check you propose doesn't tell a class from a usual function. And that's what I was googling for before finding this answer. Yet now i realize the initial question was just telling a class from an object, which is much easier. runkit.com/disjunction/581f5cff1cdd06001402ea5f
Well, yo do can do new hello() in your example. Javascript is such a poor language.
v.prototype.constructor === v is true for every non-arrow function. All functions has prototype and all functions prototype has constructor default to the function itself.
|
0

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` :(

Comments

-1

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

18 Comments

ironically in your implementation isclass(isclass) returns true.
@Joe Hildebrand good identification....sat down to bring it into more clarity and more tight testing of isclass.
@Joe Hildebrand check out new function covers almost all of the new test cases and tiny toony compared to previous one. took me a long walk to get to the short solution. thanks for update. I have kept the old update so future readers can gather on discussion as well.
You offer two different implementations, and both won't work correctly. The first one fails in strict mode, and the second is a buggy version of another answer; try: isClass('class I'm a class. Do you believe me?')
@Syed You are right, some of my comments were not concrete enough. Here are some snippets that demonstrate false negatives and false positives. You might want to learn more about the basics of types, and how functions work. The entire arguments + properties check is entirely off, since you are targeting the wrong thing with it. If it was correct, it would still not work in strict mode. For your first solution: console.log(isclass(class Thisisanullclass { }), isclass({ prototype: 'hi' })). --- For your second solution: console.log(isclass('class {}'), isclass('class' + (() => {})))
|
-1
function IsClass(obj)
{
    return Object.getOwnPropertyDescriptors(obj)?.prototype?.writable === false ? true : false;
}

https://stackoverflow.com/a/78714785/5220303

Comments

-3

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.

Comments

-5

Maybe this can help

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

3 Comments

add a typeof obj == 'function' and remove the new and you would have something that gets close... But I would recommend against calling the function to try to determine if it's an ES6 class, because 1) it may have side effects (e.g. I wonder whether triggerNuclearBomb is an ES6 class... let's call it to find out!) and 2 transpilers may optimize away the error checking for production builds meaning it would not work for transpiled code.
A perfectly valid constructor may throw an error on a missing parameter, which also would return false.
would seek your review on answer above .... i have brought to light what is being discussed here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.