A helper is declared `function register<T>(ctor: new (...args: any[]) => T): void`, and passing your abstract base class to it fails to compile. Why, and how do you type the parameter so it is accepted?
answer
- two flavours of construct signature
- new means "I may build this"
- prefix the signature with abstract
- concrete assignable to abstract, not back
- widened parameter still blocks new inside
basics
~20 sAn abstract class's constructor type carries an abstract construct signature, which is not assignable to a non-abstract one because it cannot be called with new. Type the parameter abstract new (...args: any[]) => T when the function only needs the class, not an instance.
solid answer
~50 s`new (...args: any[]) => T` means "something I can construct", and an abstract class is exactly the thing you cannot construct — so the compiler refuses, usually with the elaboration *Cannot assign an abstract constructor type to a non-abstract constructor type*. The fix depends on what the helper actually needs. If it only stores the class, keys a map by it, or derives its instance type, widen the parameter to `abstract new (...args: any[]) => T`, which accepts both abstract and concrete classes; the compiler then refuses to let you write `new ctor()` inside the body, which is precisely the guarantee you want. If the helper genuinely has to instantiate, keep the non-abstract `new (...) => T` and fix the call site instead by passing a concrete subclass — the error is telling you the truth. This is TypeScript 5.x behaviour, and the same widened form is what the standard library's `InstanceType` uses in its own constraint.
code
typescript · 19 linesabstract class Plugin {
abstract run(): void;
}
class Logger extends Plugin {
run(): void {
console.log('logging');
}
}
const registry = new Map<string, abstract new (...args: any[]) => Plugin>();
function register(key: string, ctor: abstract new (...args: any[]) => Plugin): void {
registry.set(key, ctor);
}
register('base', Plugin);
register('logger', Logger);
console.log(registry.size);go deeper
Recall that new (...) => T describes something you are allowed to construct, and that an abstract class is by definition not that, which is why the call is rejected.
Explain the two construct-signature flavours, spell the abstract new (...args: any[]) => T form correctly, and state that assignability runs from concrete to abstract only.
Decide correctly per call site: widen when the helper only stores or reflects on the class, keep the narrow type and fix the caller when it constructs, and recognise a widened parameter used to silence a real error.
Set the convention for how shared infrastructure — registries, DI containers, factory helpers — types class parameters, so that constructibility is stated in the signature rather than asserted away at each call site.
## Two kinds of constructor type When you write a class in TypeScript, the name lands in two spaces: a **type** naming the instances, and a **value** whose type describes the static side, including its construct signature. For a concrete class that construct signature is ordinary. For an abstract class it is an **abstract construct signature** — a signature that describes what the constructor's parameters and result would be, while recording that it may not be invoked with `new`. That distinction is exactly what makes the assignment fail: ```ts abstract class Plugin { abstract run(): void; } class Logger extends Plugin { run(): void { console.log('logging'); } } function register<T>(ctor: new (...args: any[]) => T): void {} register(Logger); // ok register(Plugin); // Error: Argument of type 'typeof Plugin' is not assignable to // parameter of type 'new (...args: any[]) => Plugin'. // Cannot assign an abstract constructor type to a non-abstract constructor type. ``` The parameter type is a promise: *give me something I may call with `new`*. `Plugin` cannot honour it. The compiler is not being pedantic — if it allowed the assignment, `register` could construct an instance whose `run` does not exist, and you would get a `TypeError` at some later call. ## The `abstract new` form The wider type is written by prefixing the construct signature with `abstract`: ```ts type AnyPluginClass = abstract new (...args: any[]) => Plugin; ``` Read it as "a class object whose instances are `Plugin`, constructible or not". Assignability runs one way: a concrete constructor type **is** assignable to the abstract form (a class you can construct is trivially a class you might not construct), but not the reverse. So the widened parameter accepts everything the narrow one did, plus abstract classes. ```ts const registry = new Map<string, abstract new (...args: any[]) => Plugin>(); function register(key: string, ctor: abstract new (...args: any[]) => Plugin): void { registry.set(key, ctor); } register('base', Plugin); // now ok register('logger', Logger); // still ok ``` And the guarantee is kept where it matters — inside the body, the compiler will not let you construct it: ```ts function bad<T>(ctor: abstract new (...args: any[]) => T): T { return new ctor(); // Error: Cannot create an instance of an abstract class. } ``` That error is the feature. Widening the parameter does not throw away safety; it moves the restriction from the call site to the body, which is where the restriction actually belongs when the function never intended to construct anything. ## Generic constraint form The same shape appears as a constraint when you want to preserve the exact class type rather than collapse it: ```ts function describeClass<T extends abstract new (...args: any[]) => any>(ctor: T): string { return `class with instances of the declared shape`; } ``` This is the form the standard library itself uses: `InstanceType<T>` is declared with a constraint of `abstract new (...args: any) => any`, which is why `InstanceType<typeof Plugin>` resolves rather than erroring. If the constraint were the non-abstract form, every abstract class would be locked out of the utility. ## Choosing between the two Ask what the function does with the parameter: - **Stores it, compares it, keys a map with it, or only needs its instance type** — use `abstract new (...args: any[]) => T`. Registries, dependency-injection token maps, and type-level helpers are all in this category. - **Actually calls `new`** — keep `new (...args: any[]) => T`. The error at the call site is correct information: the caller handed you something that cannot be built, and widening the parameter would only push the failure to runtime. Fix the caller by passing a concrete subclass. The second case is worth naming because widening is a tempting way to silence the error. If your factory then constructs the value, you have converted a compile-time error into a `TypeError` at 3 a.m. — the compiler will actually stop you inside the body, but only if you did not reach for an assertion to get past that too. ## Parameters that are `any[]` The `...args: any[]` in these signatures is idiomatic rather than sloppy: a helper accepting arbitrary classes cannot know their constructor arities, and a narrower parameter list would reject classes whose constructors take arguments. When the helper *does* construct, prefer to state the real parameter list — `new (config: Config) => T` — so the compiler checks the arguments you pass. ## Version note The behaviour described here is that of the TypeScript 5.x line. Older compilers had no way to spell an abstract construct signature at all, which is why pre-5.x codebases often work around it with an assertion at the call site; if you meet that pattern in an old file, the widened parameter type is the clean replacement.
- If you widen the parameter to `abstract new (...args: any[]) => T`, what can the function body no longer do?It can no longer write `new ctor(...)` — the compiler reports that you cannot create an instance of an abstract class. That is the intended trade: the parameter accepts more class objects precisely because the body promises not to construct them. If the body must construct, the narrow non-abstract type is the correct parameter.
- Which direction does assignability run between abstract and non-abstract constructor types?A concrete constructor type is assignable to the abstract form, but never the reverse. Anything you may construct is also something you might merely hold, so widening is safe; going the other way would let a caller construct something the type system declared unconstructible.
- Why does `InstanceType<typeof SomeAbstractClass>` work rather than erroring on the constraint?Because `InstanceType` is declared with an `abstract new (...args: any) => any` constraint in the standard library, so abstract class objects satisfy it. Had the constraint used the non-abstract form, every abstract class would fail it — which is exactly the bug the widened spelling was introduced to fix.
saying these in an interview costs you the question
- Says abstract classes have no static side or constructor type
- Claims the fix is an `as any` cast at the call site
- Thinks `abstract new (...) => T` still lets the body call `new`
- Believes assignability runs both ways between the two forms
- Widens the parameter in a factory that genuinely constructs the value