12.6 InstanceType<C> and ConstructorParameters<C>
Let’s be honest: you don’t wake up in a cold sweat dreaming about InstanceType<C> and ConstructorParameters<C>. They are, without a doubt, some of TypeScript’s more esoteric utilities. But that’s exactly why we’re here. When you do need them, you really need them, and understanding them feels like unlocking a secret superpower. They exist for one specific but crucial job: giving you type-safe superpowers when you’re doing metaprogramming with classes and their constructors.
Think of a class constructor not as a class, but as a function. A very special function that, when called with new, returns an instance of the class. InstanceType<C> and ConstructorParameters<C> are the tools that let you surgically extract the return type and parameter types of that special function, respectively.
The InstanceType<C> Power-Up
InstanceType<C> is the simpler of the two to grasp. You give it a constructor type C, and it gives you back the type of the instance that constructor creates. It’s the type-level equivalent of writing const instance = new Constructor().
class CatHerder {
constructor(public cats: string[], public coffeeConsumed: number) {}
herd() {
console.log(`Trying to herd ${this.cats.length} cats.`);
}
}
// We want a type that represents any instance of CatHerder
type HerderInstance = InstanceType<typeof CatHerder>;
// This is now identical to: `type HerderInstance = CatHerder;`
// Let's prove it by creating one without using the class directly.
const myHerder: HerderInstance = {
cats: ["Whiskers", "Mittens"],
coffeeConsumed: 4,
herd: () => console.log("Meow!")
};
myHerder.herd(); // "Trying to herd 2 cats."
“Why wouldn’t I just use CatHerder?” you ask. Excellent question. The power isn’t in renaming your own classes; it’s in working with classes you don’t control or that are passed around as values. Its prime use case is in higher-order functions or factory patterns.
// A function that takes ANY class constructor and creates an instance
function createInstance<T extends new (...args: any) => any>(
Constructor: T,
...args: ConstructorParameters<T>
): InstanceType<T> {
return new Constructor(...args);
}
// TypeScript now knows this returns a `CatHerder`
const newHerder = createInstance(CatHerder, ["Alfie"], 2);
newHerder.herd(); // Completely type-safe!
Deconstructing with ConstructorParameters<C>
If InstanceType gets the “what,” ConstructorParameters gets the “how.” It extracts the parameter tuple of a constructor function type, allowing you to reuse that signature elsewhere. This is incredibly useful for wrapping or proxying a constructor.
type WhatINeedToBuildACatHerder = ConstructorParameters<typeof CatHerder>;
// type WhatINeedToBuildACatHerder = [cats: string[], coffeeConsumed: number]
// Let's use it in our factory function from before:
function createCatHerder(...args: ConstructorParameters<typeof CatHerder>): CatHerder {
return new CatHerder(...args);
}
const herderArgs: ConstructorParameters<typeof CatHerder> = [["Zorro", "Jack"], 3];
const anotherHerder = createCatHerder(...herderArgs); // Perfectly type-checked
Navigating the Pitfalls
Here’s where the designers’ questionable choices, or at least the inherent complexity of JavaScript, come into play. These utilities only work on constructor types, not on instance types. This is the most common “gotcha.”
declare const someInstance: CatHerder;
// This is a massive error. `CatHerder` is an instance type, not a constructor type.
type ThisWillError = InstanceType<typeof someInstance>; // ❌
type ThisAlsoErrors = ConstructorParameters<CatHerder>; // ❌
// This is correct. `typeof CatHerder` refers to the class constructor itself.
type ThisWorks = InstanceType<typeof CatHerder>; // ✅
Another edge case is with abstract classes. The TypeScript team made a brilliant choice here: they actually work with abstract constructors.
abstract class AbstractDatabase {
constructor(public connectionString: string) {}
abstract connect(): void;
}
// This correctly creates a tuple of the constructor params, even though
// you can't directly `new` an abstract class.
type DatabaseParams = ConstructorParameters<typeof AbstractDatabase>; // [connectionString: string]
// And this correctly resolves to the abstract class instance type.
type DatabaseInstance = InstanceType<typeof AbstractDatabase>; // AbstractDatabase
This means you can use these types to define functions that are meant to work with the subclasses of an abstract class, ensuring they use the right constructor signature and return the correct base instance type. It’s meta, but it’s powerful, correct, and exactly the kind of type safety that saves you from a night of debugging.
So, while you might not use these utilities every day, file them away in your mental toolbox. When you inevitably need to write a generic factory, a decorator, or any other higher-order class logic, you’ll be profoundly grateful they exist. They turn a potential any-filled nightmare into a elegantly typed solution.