Repository navigation
type Exclude on class hierarchy resolves to never #64957
Description
🔎 Search Terms
typescript Exclude on class hierarchy resolves to never
🕗 Version & Regression Information
- I first experienced this behaviour in version 6.0.3.
- For interfaces, it seems to date back to at least 3.2.2.
- I tested version 3.3.3333 on typescript playground exhibiting the same behaviour.
- Could not test typescript 7.0 because playground does not support it.
⏯ Playground Link
💻 Code
class A {
constructor(public x: number, public y: number) {}
}
class B extends A {
constructor(a: A, public b: string) {
super(a.x, a.y);
}
}
class C extends A {
constructor(a: A) {
super(a.x, a.y);
}
}
class CMod extends A {
constructor(a:A, public dummy: number = 0) {
super(a.x, a.y);
}
}
type All = B | C;
type AllMod = B | CMod
type Some = Exclude<All, C>; // Expected: B, Actual: never
type SomeMod = Exclude<AllMod, CMod>; // B as expected🙁 Actual behavior
type Some = Exclude<All, C> yielded never, even though C is a class distinct from A.
If they were interfaces as shown in Stackoverflow issues [1], [2], I'd understand the behaviour as interfaces are matched structurally. But classes should bear distinct type information that makes them discriminable even if their members are the same.
[1] https://stackoverflow.com/questions/54009493/why-does-typescript-give-type-never-when-excluding-extended-interface
[2] https://stackoverflow.com/questions/63185800/typescript-excludeunionoftypes-interface-is-type-never
🙂 Expected behavior
type Some = Exclude<All, C> yields B as does SomeMod.
Additional information about the issue
Given that the behaviour exists since at least 3.3.3333 I'm afraid this is intended.
However, I don't understand why classes do not differ from interfaces from a type system perspective.
I don't like the CMod solution by polluting every instance with an additional dummy member just to make the type system work as expected.
Activity
jcalz commentedon Oct 25, 2026
Note well: I'm not a TS team member
TypeScript's type system is structural and the "class"-ness of a type doesn't affect its structure. See Relationships Between Classes in the handbook. If you want types to differ you should make them differ structurally, and adding different members is one way to do this.
Everything in your example is behaving as intended so there's no bug here. If you're used to nominal typing then your expectations will sometimes be subverted.
If this were Stack Overflow I'd go on to ask about your use case and try to figure out if this is just a mismatch of expectations, or if it's causing some more concrete problem (since TS isn't perfectly sound and can allow unsafe things to happen) and how we could best deal with it. But it's the GitHub issue tracker and the behavior is intentional so I won't digress further.
xtnw commentedon Oct 25, 2026
So my gut feeling was right, and this seems to be intended behaviour. Feel free to close this bug as wontfix.
Now I only have to find a way to retain sound typing without bloating my classes with unused properties at runtime. But this is, as Joe Calzaretta (@jcalz) correctly stated, not part of this bug report.
irfanstract commentedon Oct 25, 2026
for now, make the following change (add some private fields):
class A { constructor(public x: number, public y: number) {} + #A: any } class B extends A { constructor(a: A, public b: string) { super(a.x, a.y); } + #B: any } class C extends A { constructor(a: A) { super(a.x, a.y); } + #C: any }jcalz commentedon Oct 25, 2026
But that suggestion is adding unused properties at runtime, directly counter to OP's requirement.
Instead they could use type-only class fields which have no runtime impact:
Playground
which might be sufficient, depending on the use case.
But again, in the absence of a use case I'd refrain from suggesting anything. I'd really want to see something concrete that goes wrong at runtime if you just allow the assignability of
AtoC. Maybe something involvinginstanceof? Like, why do you think you need to distinguish two structurally identical types? You might have a legitimate need, but it's not demonstrated here. I don't mind discussing such things here but it really isn't the place for it.