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.
Table of Contents
- 1. The Problem: new (...) => T Fails for Abstract Classes
- 2. Basic Syntax: the abstract new (...) => T Type
- 3. Practical Example: a Factory Function for Subclasses
- 4. Generic Constraints with abstract new (...args: any[]) => any
- 5. Comparison to Mixin Functions
- 6. Interaction with Static Methods and Members
- 7. Common Pitfalls
- 8. Best Practices for abstract new
- 9. Conclusion: When abstract new (...) => T Is the Right Type
- 10. Summary
- 11. FAQ
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