TypeScript: Get the name of a class instance at runtime

Updated: January 8, 2024 By: Guest Contributor Post a comment

Introduction

TypeScript adds static type checking to JavaScript, but certain dynamic features, such as obtaining a class instance’s name at runtime, can still be very useful when working in TypeScript. Whether for debugging or serialization purposes, this guide will take a deep dive into how you can achieve this.

Understanding Instance Type Information

In TypeScript, just as in JavaScript, every class instance has a constructor property that is a reference to the constructor function that created the instance. If the class has a name, you can access it via this constructor property. Here’s the basic idea using plain JavaScript:

class MyClass {
    // Constructor, methods, etc.
}
let myInstance = new MyClass();
console.log(myInstance.constructor.name); // Outputs: "MyClass"

This works well in most cases. However, TypeScript introduces a few complications, especially when it comes to minification or inheritance.

Fetching Class Names with TypeScript

Let’s translate that basic example into a TypeScript context. It’s largely the same:

class MyClass {
    // TypeScript constructor, methods, etc.
}
let myInstance: MyClass = new MyClass();
console.log(myInstance.constructor.name); // Outputs: "MyClass"

This syntax is straightforward but keep in mind that if you minify your code for production, it’s likely that the class names will be mangled, which could alter the output.

Handling Minification

When your TypeScript code is minified, the names of classes may be shortened to reduce the size of the code. This will affect your ability to get the classname from the instance constructor. One workaround is to add a readonly property to the class that returns the class name:

class MyClass {
    public readonly className = 'MyClass';
    // Rest of the class
}

let myInstance = new MyClass();
console.log(myInstance.className); // Outputs: "MyClass"

This ensures that the name is preserved after minification.

Dealing with Inheritance

Things get trickier when you deal with inheritance. Consider the following TypeScript example:

class ParentClass {
    public get className(): string {
        return this.constructor.name;
    }
}

class ChildClass extends ParentClass {
    // Child class specifics
}

let childInstance = new ChildClass();
console.log(childInstance.className); // Outputs: "ChildClass"

This is handy because it deals with inheritance out of the box – the child class instance will correctly identify itself as a ‘ChildClass’. However, this approach is still vulnerable to minification issues.

Class Decorators for Class Name

To make our approach minification-proof and maintainable for large codebases, we can use class decorators to assign static properties to our classes:

function ClassNameDecorator(constructor: T) {
    constructor.prototype.className = constructor.name;
}

@ClassNameDecorator
class MyClass {}

let myInstance = new MyClass();
console.log(myInstance['className']); // Outputs: "MyClass"

This decorator attaches a className property directly to the class prototype, which ensures that each instance of the class will have access to this property without it being enumerated or altered via minification.

Type Guards and Instance Names

In some advanced use cases, you might want to use type guards in conjunction with instance names. TypeScript has the ‘instanceof’ operator which checks if an object is an instance of a particular class.

class MyClass {}

class MyOtherClass {}

let myInstance: MyClass | MyOtherClass = new MyClass();

if (myInstance instanceof MyClass) {
    console.log('Instance of MyClass');
} else {
    console.log('Instance of MyOtherClass');
}

However, there are times when you might need to do this dynamically. One approach is to use a type guard function:

function isInstanceOf(instance: any, className: string): boolean {
    return instance.constructor.name === className;
}

let myInstance: MyClass | MyOtherClass = new MyClass();
if (isInstanceOf(myInstance, 'MyClass')) {
    console.log('Instance of MyClass');
} else {
    console.log('Instance of MyOtherClass');
}

Advanced Serialization

When dealing with serialization and deserialization, retaining type information can be crucial. Using the aforementioned strategies will be essential in correctly serializing and deserializing class instances.

The following could be an advanced approach:

class Serializable {
    toJSON () {
        return {...this, typeName: this.constructor.name };
    }
}

class MyClass extends Serializable {}

let myInstance = new MyClass();
console.log(JSON.stringify(myInstance)); // The output will include the "typeName": "MyClass" property and value.

Conclusion

This guide has explored different ways to retrieve the class name of an instance at runtime in TypeScript. From basic examples to more advanced scenarios, each has its place depending on your needs, considering factors like inheritance and minification. Real-world applications may require a combination of these approaches to address various challenges in TypeScript class identification.