skip to content

Construct Signatures and the Static Side

A construct signature `new (...args) => T` types the constructor itself, which is a different type from the instances it produces. Interviewers use it to test whether you can separate a class's static side from its instance side when typing factories, DI containers, or mixin helpers.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

4

In TypeScript, a helper is supposed to receive the `Person` class itself, but `function make(ctor: Person)` rejects the argument `Person`. What does the type `Person` actually refer to, and how should the parameter be typed?

level: juniorimportance: must knowfreq 55%

answer

  1. one name, two declaration spaces
  2. type position means an instance
  3. value position means the constructor
  4. typeof in a type position
  5. or spell out new (...) => Person

basics

~20 s

A class declaration creates two separate things: a type named Person that describes instances, and a value named Person that is the constructor. A parameter receiving the class itself must be typed typeof Person, or new (name: string) => Person.

solid answer

~40 s

`class Person {}` declares a name in two different spaces. In a *type* position, `Person` means "an instance of Person" — it has the instance members. In a *value* position, `Person` is the constructor function, and its type is written `typeof Person` — the static side, carrying static members and a construct signature. So `function make(ctor: Person)` is asking for an instance, and passing the class fails with something like "Type 'typeof Person' is missing the following properties from type 'Person'". Type the parameter `typeof Person` to accept exactly that class, or `new (name: string) => Person` to accept any constructor with that shape. Interfaces and type aliases have no value side at all, which is why you cannot pass one as an argument.

code

typescript · 18 lines
typescript
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  static species = "human";
}

// `Person` in a type position: an instance
const instance: Person = new Person("Ada");

// the class value itself: the static side
function make(ctor: typeof Person, name: string): Person {
  return new ctor(name);
}

console.log(make(Person, "Grace").name);
console.log(Person.species); // static lives on the constructor, not the instance

go deeper

for a junior

Be ready to say plainly that a class name used as a type means an instance, and that the class value itself is typed typeof ClassName. Practise reading the error that names both.

for a middle

Explain the two declaration spaces and what typeof yields: the static side with statics plus a construct signature. Contrast typeof Person with a hand-written new (name: string) => Person and say when each is the right parameter type.

for a senior

Show judgment about which contract to publish in an API: the exact class, a construct signature, or an already-built instance. Explain why accepting a constructor makes the caller's lifecycle your problem.

for a principal

Own the design tradeoff: taking constructors couples your API to instantiation and makes substitution, testing and lazy construction harder than taking instances or a factory function. Argue when that coupling is worth it.

## Two names, two meanings TypeScript keeps declarations in separate *declaration spaces*: a type space and a value space. Most declarations land in only one. An `interface` or a `type` alias exists only in type space — writing it where a value is expected gives "only refers to a type, but is being used as a value here". A `const` exists only in value space. A **class** is the common declaration that lands in both, and this dual nature is what the question is really about. ```ts class Person { name: string; constructor(name: string) { this.name = name; } static species = "human"; } const p: Person = new Person("Ada"); // `Person` in a TYPE position = an instance const C = Person; // `Person` in a VALUE position = the constructor ``` The instance type `Person` has `name`. It does **not** have `species`, because static members live on the constructor object, not on instances. That is the whole static-side / instance-side split. ## What `typeof Person` gives you In a type position, `typeof <value>` is the **type query** operator: it asks the compiler for the type of an existing value. Applied to a class it yields the static side — a type that has the class's static members plus a construct signature describing how to `new` it. ```ts function make(ctor: typeof Person, name: string): Person { return new ctor(name); } make(Person, "Ada"); ``` This type query is a compile-time operator that happens to reuse the `typeof` keyword; it is not the runtime `typeof` expression, and it is erased along with everything else in the type layer. ## Writing the constructor type by hand You do not have to name a class to describe a constructor. A **construct signature** spells the shape out: ```ts function make(ctor: new (name: string) => Person, name: string): Person { return new ctor(name); } ``` `typeof Person` says "this exact class (or a subclass)", including its statics. `new (name: string) => Person` says "anything you can `new` with a string to get a Person" — a looser contract that also accepts an unrelated class or a class expression with the right shape. Prefer the construct signature when you want to accept several classes; prefer `typeof Person` when the callee also uses the statics. ## Why the original annotation failed `function make(ctor: Person)` demands an object with a `name: string` property. The class value has `species`, `prototype` and a construct signature, but no `name` in the instance sense, so the assignment is rejected. The error text names both sides — `typeof Person` on the left, `Person` on the right — and once you can read that pair, the fix is obvious. The mirrored mistake is just as common: annotating something `typeof Person` and then trying to read `.name` off it, or calling it without `new`. ## Going back the other way Given a constructor type, `InstanceType<T>` recovers the instance type, so `InstanceType<typeof Person>` is just `Person`. That round trip is mostly useful when the class has no exported name — for example a class expression stored in a `const`: ```ts const Point = class { x = 0; y = 0; }; type Point = InstanceType<typeof Point>; // the instance type now has a name ``` ## Why this matters beyond the annotation Factories, dependency-injection containers, plugin registries and test helpers all pass classes around as values. Every one of them has to decide whether the parameter means "the thing" or "the maker of the thing". Getting that wrong is the single most common TypeScript error when a class first crosses a function boundary — and none of it survives compilation, since the type layer is erased and only the ordinary JavaScript class value is emitted.

  • If a class has no name because it was written as a class expression, how do you name its instance type?
    Store the expression in a `const`, then use `InstanceType<typeof TheConst>`. The `typeof` query gets the constructor type of the value, and `InstanceType` extracts what `new` on it produces. You can then declare a type alias with the same name as the const, since types and values occupy separate declaration spaces.
  • Why can't you pass an interface where a value is expected?
    An interface exists only in type space and emits no JavaScript, so there is no runtime value to pass. The compiler says it "only refers to a type, but is being used as a value here". If you need something at runtime you need a class, a const, or a plain object that conforms to the interface.
  • What is the practical difference between typing a parameter `typeof Person` and `new (name: string) => Person`?
    `typeof Person` accepts that class and its subclasses and keeps the static members visible to the callee. The construct signature accepts any class whose constructor matches, including unrelated classes and class expressions, but exposes no statics. Use the construct signature when you want a shape contract, `typeof` when you also need the statics.

saying these in an interview costs you the question

  • Says the class type includes its static members
  • Thinks typeof Person is the runtime typeof operator
  • Claims an interface can be passed as an argument value
  • Believes new () => Person and typeof Person are interchangeable
  • Says you must use any to pass a class around

context

open as a page

In TypeScript, what does the type `new (name: string) => Person` describe, and which values are actually assignable to it?

level: middleimportance: must knowfreq 58%

basics

~20 s

It is a construct signature: it describes a value you invoke with new, taking a string and producing a Person. Classes and class expressions with a matching constructor are assignable; plain functions and arrow functions are not.

open as a page

In TypeScript, why does passing an abstract class to a parameter typed `new () => Shape` fail to compile, and how do you type the parameter so it accepts one?

level: middleimportance: should knowfreq 38%

basics

~20 s

An abstract class cannot be instantiated, so its constructor type is an abstract constructor type, which TypeScript refuses to assign to a plain new () => Shape. Type the parameter abstract new () => Shape, which accepts abstract and concrete classes alike.

open as a page

In TypeScript, you want every class implementing your `Codec` interface to also expose a static `fromJSON` method. Why does putting it in the interface and writing `class Doc implements Codec` fail to enforce that, and what does enforce it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

An implements clause checks only the instance side, so static members and construct signatures written in the interface are never enforced on the class. Declare a separate static-side interface and check the class value against it, with an annotated const or a satisfies expression.

open as a page