Skip to Content
Course content

189: InstanceType<Type>

Click on the "Edit" button in the top corner of the screen to edit your slide content.

There is a specific kind of frustration that hits when you're building a factory pattern in TypeScript. You have a set of classes, and you want a function that can take one of those classes as an argument, instantiate it, and return it. On the surface, it seems simple, but you'll quickly find that TypeScript is very picky about the distinction between a class (the constructor function) and an instance of that class.

The "any" escape hatch and its cost

When I first encountered this, I did what most of us do when the compiler starts screaming: I used any. Imagine we're building a simple game engine where we have different types of Entity classes, like Player and Enemy. We want a Spawner class that can create any of these.

class Entity {
  id = Math.random();
}

class Player extends Entity {
  health = 100;
  heal() { this.health++; }
}

class Enemy extends Entity {
  damage = 20;
  attack() { console.log("Attacking!"); }
}

class Spawner {
  // The naive way: just use 'any' or the base class
  spawn(entityClass: any): any {
    return new entityClass();
  }
}

const spawner = new Spawner();
const player = spawner.spawn(Player);
// The compiler has no idea 'player' is actually a Player instance.
// You lose all autocomplete and type safety here.

The problem here is obvious. By using any, we've essentially turned off TypeScript. If I try to call player.heal(), the IDE won't help me, and if I typo it as player.heall(), the compiler won't catch it. You might think, "Why not just type the return as Entity?" Well, that's better, but then you can't access health or damage without a manual type cast, which is just any with extra steps.

Fighting the constructor generic

You might try to solve this with a generic, like this: spawn<T>(cls: T): T. But this is where the mental model usually breaks. In TypeScript, if T is a class, then T refers to the constructor function itself, not the object that comes out of the new keyword. If you return T, you're telling TypeScript you're returning the class definition, not the instance.

To fix this, you need a way to tell TypeScript: "I am taking a constructor, and I want to return whatever type that constructor produces." This is exactly why InstanceType<T> exists.

Linking constructors to instances

The InstanceType<T> utility takes a constructor type and extracts the type of the instance it creates. To make this work in a factory, we combine it with a generic constraint that ensures the passed-in argument is actually a constructable class.

class Spawner {
  // We constrain T to be a constructor that takes any arguments and returns any object
  spawn<T extends new (...args: any[]) => any>(entityClass: T): InstanceType<T> {
    return new entityClass();
  }
}

const spawner = new Spawner();
const player = spawner.spawn(Player); 
// Now 'player' is automatically typed as Player!
player.heal(); // Perfectly safe and autocompleted.

const enemy = spawner.spawn(Enemy);
enemy.attack(); // Perfectly safe.

I love this approach because it's completely dynamic. You don't have to maintain a massive union type of every single entity in your game. If you add a Boss class tomorrow, the Spawner handles it automatically. The InstanceType<T> helper acts as the bridge between the static class definition and the live object instance, keeping your type chain intact from the moment of creation to the moment of usage.




📋 Practical Task

Implement a Typed Dependency Injector

You are building a simplified Dependency Injection (DI) container. You have a Container class that stores "providers" (the classes themselves) and a resolve method that instantiates those classes on demand.

Currently, the resolve method returns any, which is causing bugs in the production code because developers are calling methods that don't exist on certain services.

Your Task: Modify the resolve method in the Container class to use a generic constraint and InstanceType<T> so that the returned object is correctly typed based on the class passed into the method.

class DatabaseService {
  connect() { console.log("Connected to DB"); }
}

class LoggerService {
  log(msg: string) { console.log(msg); }
}

class Container {
  private services = new Map();

  register(name: string, service: any) {
    this.services.set(name, service);
  }

  // FIX THIS METHOD:
  resolve(serviceClass: any): any {
    return new serviceClass();
  }
}

const container = new Container();
container.register("db", DatabaseService);

// After your fix, 'db' should be inferred as DatabaseService
const db = container.resolve(DatabaseService);
db.connect(); 
Rating
0 0

There are no comments for now.

to be the first to leave a comment.