TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
189: InstanceType<Type>
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();
There are no comments for now.