Abstract Construct Signatures in TypeScript Explained
AI generated
type
TypeScript · Generics
Abstract Construct Signatures: Passing Abstract Classes as Type-Safe Parameters
The abstract new (...) => T type since TypeScript 4.2

Anyone building a factory function, a mixin, or a small dependency injection container quickly runs into a problem: the type new (...) => T only accepts concrete, instantiable classes. Abstract base classes fall through the cracks, even though they are exactly what you often need when processing subclasses generically. That is precisely what the abstract construct signature, introduced in TypeScript 4.2, was built for.

10 min read TypeScript 4.2+ Generics Factory Pattern

1. The Problem: new (...) => T Fails for Abstract Classes

The classic constructor type new (...args: any[]) => T describes something that can be called with new. That is exactly the limitation: TypeScript checks at this point whether the type is actually instantiable. An abstract class, by definition, is not, since it may contain missing implementations and must not be created directly with new at runtime.

This leads to a concrete compiler error as soon as you try to pass an abstract class as a value for a parameter typed new (...) => T. The error message roughly states that the abstract constructor type is not assignable to the concrete constructor type. Before TypeScript 4.2, developers often had to work around this check with any or unsafe type assertions, which obscured the actual intent of the code.

Yet the underlying need is legitimate: a function should be able to accept a base class as a blueprint without needing to know or care whether that base class is abstract. Only the abstract construct signature lets this distinction be expressed cleanly in the type system instead of silently ignoring it.


abstract class Repository {
  abstract findAll(): unknown[];
}

function createInstance<T>(ctor: new () => T): T {
  return new ctor();
}

// Error: abstract constructor type "typeof Repository"
// is not assignable to concrete constructor type "new () => T".
createInstance(Repository);

2. Basic Syntax: the abstract new (...) => T Type

Since TypeScript 4.2, the keyword new in a function type can be prefixed with the modifier abstract: abstract new (...args: any[]) => T. This type describes a constructor that may originate from either a concrete or an abstract class. It is therefore strictly broader than new (...args: any[]) => T.

The direction of assignability matters: every concrete class can be assigned without issue to a variable typed abstract new (...) => T, since an instantiable class automatically satisfies the weaker requirements of the abstract constructor type. The reverse does not work: a value typed abstract new (...) => T cannot be assigned to the concrete type new (...) => T, because the compiler cannot guarantee that an instantiable class is actually behind it.

This asymmetry is intentional and mirrors the runtime semantics exactly: you may accept more types with less information, but you cannot automatically infer concrete instantiability from a more general type.


abstract class Animal {
  abstract makeSound(): string;
}

class Dog extends Animal {
  makeSound(): string {
    return "Woof";
  }
}

let ctor: abstract new () => Animal;

ctor = Animal; // allowed: abstract class
ctor = Dog;    // allowed: concrete class is compatible too

let concreteCtor: new () => Animal;
// concreteCtor = ctor; // Error: abstract new is not assignable to new
concreteCtor = Dog;     // allowed: Dog is concrete

3. Practical Example: a Factory Function for Subclasses

The classic use case is a factory function that accepts a base class as a configuration parameter but never attempts to instantiate that base class directly. Instead, the function expects a concrete subclass to eventually be passed in, or it combines the base class with additional logic before creating a concrete class.

In the example below, createRepository accepts a constructor typed abstract new () => Repository. Inside the function, the abstract base class itself is never instantiated; only the supplied concrete constructor is, which is guaranteed at runtime to be a subclass of Repository. The return type stays bound to the base class, which is sufficient for the caller, since it only ever programs against the abstract interface anyway.

This pattern is especially useful in dependency injection containers: the container only knows the abstract base class as a contract at registration time, but receives a concrete implementation at runtime that it can instantiate without any further type assertions.


abstract class Repository {
  abstract findAll(): unknown[];
}

class InMemoryRepository extends Repository {
  private items: unknown[] = [];

  findAll(): unknown[] {
    return this.items;
  }
}

function createRepository<T extends Repository>(
  ctor: abstract new () => T,
  concreteCtor: new () => T,
): T {
  // ctor only serves as a contract/documentation, only the
  // guaranteed concrete constructor is ever instantiated.
  return new concreteCtor();
}

const repo = createRepository(Repository, InMemoryRepository);
console.log(repo.findAll());

4. Generic Constraints with abstract new (...args: any[]) => any

In practice, abstract new is most often encountered not as a concrete parameter type but as a constraint on a generic type parameter: <T extends abstract new (...args: any[]) => any>. This formulation accepts any constructor, abstract or concrete, regardless of how many arguments it expects and regardless of which instance type it produces.

The advantage of this broad constraint is that generic helper functions do not need to artificially restrict themselves to concrete classes when they are actually only interested in the shape of the produced instance. Using InstanceType<T>, the concrete instance type can subsequently be extracted from the constructor type, regardless of whether T was abstract or concrete.

This combination of an abstract constructor type as a constraint plus InstanceType as an extraction tool underlies many generic utility functions in larger TypeScript codebases, for example for serialization, validation, or registration mechanisms.


type AnyConstructor = abstract new (...args: any[]) => any;

function getClassName<T extends AnyConstructor>(ctor: T): string {
  return ctor.name;
}

abstract class Shape {
  abstract area(): number;
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }
  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

getClassName(Shape);  // "Shape", also works for abstract classes
getClassName(Circle); // "Circle"

type CircleInstance = InstanceType<typeof Circle>; // Circle

5. Comparison to Mixin Functions

Classic mixin functions in TypeScript work with the type new (...args: any[]) => {}. This type explicitly requires a concrete, instantiable constructor, because a mixin function typically creates a new class that extends the passed base class directly: class Mixed extends Base { ... }. Here, an abstract base class would syntactically work as an extension target, but the resulting class would then itself need to implement all abstract members again.

The difference from the factory function shown earlier lies in how the constructor is used: a mixin extends the base class via extends and produces a new class from it, while a factory function typically creates an instance directly via new. Extending via extends also works with abstract classes as a starting point, as long as a concrete, fully implemented class results at the end.

Some more advanced mixin libraries therefore combine both concepts: they accept abstract new (...args: any[]) => any as a constraint, to also allow abstract base classes as a starting point for extension, but themselves only ever produce concrete, fully implemented classes as the result.


type Constructor<T = {}> = new (...args: any[]) => T;
type AbstractConstructor<T = {}> = abstract new (...args: any[]) => T;

function Timestamped<TBase extends AbstractConstructor>(Base: TBase) {
  abstract class TimestampedClass extends Base {
    createdAt = new Date();
  }
  return TimestampedClass;
}

abstract class Entity {
  abstract id: string;
}

// Mixin built on an abstract class, result stays abstract
// as long as "id" is not implemented.
class TimestampedEntity extends Timestamped(Entity) {
  id = "entity-1";
}

6. Interaction with Static Methods and Members

Since TypeScript 4.2, abstract classes can also declare abstract static members that belong to the constructor type itself. When a factory function accepts a constructor typed abstract new () => T, it can still access static methods and properties of that constructor, as long as they are declared in the type. The access works identically to concrete constructors, since static members do not depend on whether the class is instantiable.

This is particularly useful for registration patterns: an abstract base class defines an abstract static method such as fromJSON, each subclass implements it concretely, and a generic deserialization function can call this method through the constructor type without ever instantiating the base class itself.

Accessing static members through an abstract new type is an area where the typing clearly diverges from purely structural approaches based on object literals: you get real class semantics including static inheritance, without requiring instantiability.


abstract class Serializable {
  abstract toJSON(): unknown;
  static fromJSON(_data: unknown): Serializable {
    throw new Error("Must be implemented in subclass");
  }
}

class User extends Serializable {
  constructor(public name: string) {
    super();
  }
  toJSON(): unknown {
    return { name: this.name };
  }
  static fromJSON(data: { name: string }): User {
    return new User(data.name);
  }
}

function deserialize<T extends Serializable>(
  ctor: abstract new (...args: any[]) => T,
  data: unknown,
): T {
  // Access the static method through the constructor type,
  // never instantiating "ctor" itself.
  return (ctor as unknown as typeof Serializable).fromJSON(data) as T;
}

7. Common Pitfalls

The most frequent mistake is trying to call new directly on a variable typed abstract new (...) => T. The compiler correctly and consistently rejects this, because at compile time it is not known whether the underlying value is concrete or abstract. The error roughly states that abstract constructor types cannot be invoked with new. Trying to work around this rule with a type assertion defeats the compiler check, but for an actually abstract class it leads to a genuine runtime error.

A second pitfall is mixing up abstract new as a parameter type with the expectation that callers will automatically supply a concrete subclass. TypeScript does not enforce this on its own: if a function needs both an abstract class as a contract and a separate concrete constructor, both must be typed explicitly as separate parameters, as shown in the factory example above.

A third, subtler pitfall concerns the direction of assignability: developers sometimes expect an abstract new type to also be assignable to a concrete new type, since intuitively any concrete class also satisfies abstract properties. The type system deliberately does not allow this assignment in that direction, because otherwise the guarantee of instantiability would be lost.


abstract class Base {
  abstract doWork(): void;
}

function run(ctor: abstract new () => Base) {
  // Error: cannot use "new" on an abstract constructor type.
  const instance = new ctor();
  instance.doWork();
}

function runUnsafe(ctor: abstract new () => Base) {
  const AnyCtor = ctor as new () => Base;
  const instance = new AnyCtor(); // compiles, but crashes at runtime
  // if "ctor" is in fact an abstract class.
  instance.doWork();
}

8. Best Practices for abstract new

Use abstract new (...) => T whenever a function treats a class purely as a type-safe contract, for example to read its static members, use it as a constraint for generic types, or use it as a base for an extends extension. As soon as the function actually needs to instantiate the class, it needs an additional, concrete new (...) => T parameter that explicitly derives from the abstract base class.

Define reusable type aliases such as type Constructor<T> = new (...args: any[]) => T and type AbstractConstructor<T> = abstract new (...args: any[]) => T centrally in a shared utility module. This reduces repetition and makes the intent immediately visible at every usage site, without every file having to rewrite the full signature type.

Avoid using type assertions to artificially collapse the distinction between abstract and concrete. If the compiler demands a concrete constructor at a given point, that is almost always a signal that the function actually needs a second, concrete parameter, instead of forcing the existing type through an assertion.


// utility-types.ts
export type Constructor<T = object> = new (...args: any[]) => T;
export type AbstractConstructor<T = object> =
  abstract new (...args: any[]) => T;

// container.ts
import type { AbstractConstructor, Constructor } from "./utility-types";

class Container {
  private bindings = new Map<AbstractConstructor, Constructor>();

  register<T extends object>(
    contract: AbstractConstructor<T>,
    implementation: Constructor<T>,
  ): void {
    this.bindings.set(contract, implementation);
  }

  resolve<T extends object>(contract: AbstractConstructor<T>): T {
    const implementation = this.bindings.get(contract) as Constructor<T>;
    return new implementation();
  }
}

9. Conclusion: When abstract new (...) => T Is the Right Type

The abstract construct signature closes a real gap in the type system that, before TypeScript 4.2, could only be worked around through unsafe detours. Wherever a function treats a class purely as a blueprint or contract without instantiating it itself, abstract new (...) => T is the more precise and honest type compared to the concrete new (...) => T.

As soon as actual instantiation is required, the concrete constructor type remains the right choice, often as an additional, separately typed parameter alongside the abstract one. This clear separation between contract and instantiability makes factory functions, mixins, and dependency injection containers not only more type-safe but also self-documenting: anyone reading a parameter's type immediately knows whether instantiation is possible at that point or not.

Trait new (...) => T abstract new (...) => T
Accepts concrete classes Yes Yes
Accepts abstract classes No, compiler error Yes
Direct new call allowed Yes No, compiler error
Assignable to new (...) => T Yes, to itself No, only the reverse
Typical use case Direct instantiation Contract, constraint, mixin base
Access to static members Yes Yes

Mironsoft

TypeScript migration, type safety, and team onboarding

A JavaScript codebase without type safety, but no time for a full migration?

We migrate existing JavaScript projects to TypeScript step by step, set up strict compiler settings cleanly, and bring teams to the same type-safety level with code reviews and style guides.

Migration Roadmap

Plan and execute a gradual JS-to-TS migration without big-bang risk.

Strict Mode Rollout

Set up tsconfig.json, ESLint rules, and CI checks for lasting type safety.

Team Onboarding

Bring developers up to speed on TypeScript best practices with workshops and reviews.

10. Summary

Abstract Construct Signatures

Since version

TypeScript 4.2

Core idea

Constructor type that accepts both abstract and concrete classes

Direct instantiation

Not allowed, compiler forbids new on abstract new

Main use

Factory functions, mixins, DI containers

11. FAQ: Abstract Construct Signatures

1What exactly is an abstract construct signature in TypeScript?
An abstract construct signature is a function type of the form abstract new (...args) => T. It describes a constructor that may originate from either an abstract or a concrete class. Unlike the regular constructor type new (...args) => T, it does not guarantee that the described value can be instantiated directly with new.
2Why does TypeScript reject new (...) => T for abstract classes?
Because new (...) => T expresses an instantiation guarantee: the compiler ensures that every value of this type can actually be called with new. An abstract class does not satisfy this guarantee, since it cannot be instantiated directly at runtime, so the compiler rejects the assignment already at compile time.
3Can I call new directly on a value typed abstract new?
No. The compiler forbids a direct new call on a variable or parameter typed abstract new (...) => T, because at compile time it is not known whether the concrete value behind it is instantiable. Instead, you must ensure that at the point of actual instantiation a concrete constructor is present.
4How do I combine abstract new with generic constraints?
The common formulation is T extends abstract new (...args: any[]) => any. This constraint accepts any constructor, whether abstract or concrete, regardless of the number of arguments. InstanceType can then be used to extract the corresponding instance type.
5How does abstract new differ from the signature used by classic mixin functions?
Classic mixin functions use new (...args: any[]) => {}, because they extend the passed class via extends and ultimately produce a new, concrete class. Modern mixin libraries sometimes use abstract new (...args: any[]) => any as a constraint to also allow abstract base classes as a starting point, as long as the result of the extension remains concrete.
6Can I call static methods through an abstract new type?
Yes. Static methods and properties of a constructor are independent of the class's instantiability and can be accessed through an abstract new type the same way as through a concrete constructor type, as long as they are declared in the type.
7How do I build a factory function that expects both an abstract base class and a concrete subclass?
Define two separate parameters: one typed abstract new (...) => T for the base class as a contract, and one typed new (...) => T for the subclass that is actually to be instantiated. Inside the function, only the concrete parameter is ever called with new.
8Is it safe to cast an abstract new type to new (...) => T using a type assertion?
No, that deliberately bypasses type checking. If the underlying value is actually an abstract class, the subsequent new call results in a genuine runtime error. Type assertions should only be used here when the surrounding logic guarantees beyond doubt that a concrete class is involved.
9Does abstract new work together with abstract static members?
Yes, since TypeScript 4.2 abstract classes can declare abstract static members. A constructor type of abstract new (...) => T can reference these static members in a type-safe way, while concrete subclasses are required to actually implement them.
10When should I use new (...) => T instead of abstract new (...) => T?
Whenever a function actually needs to instantiate the passed class with new and should not accept abstract base classes as valid input. If the class is instead only a contract, a constraint basis, or a mixin base, abstract new (...) => T is the more precise and safer choice.