Summary
In compiled mode, when a derived class overrides a base method by adding one or more trailing optional / default parameters (a valid TypeScript override — the signature stays assignable), a call made through a base-typed reference dispatches to the base method instead of the derived override. The derived method's added default therefore never fires, and the override is effectively invisible to base-typed call sites. The interpreter is correct.
This is not a default-parameter-firing bug: it reproduces with a reference-type default too (so the default mechanism itself is fine), and a direct call on the derived static type works. The root cause is virtual dispatch to a wider-arity override.
Repro
// value-type added default
class Base { m(x: number): number { return x; } }
class Derived extends Base { m(x: number, y: number = 100): number { return x + y; } }
const d = new Derived();
console.log(d.m(3)); // A interp: 103 compiled: 103 (direct call — OK)
const b: Base = new Derived();
console.log(b.m(3)); // B interp: 103 compiled: 3 (BUG — dispatches to Base.m)
// reference-type added default — shows it is NOT default-firing-specific
class B2 { g(x: number): string { return "x" + x; } }
class D2 extends B2 { g(x: number, s: string = "Z"): string { return "x" + x + s; } }
const b2: B2 = new D2();
console.log(b2.g(3)); // C interp: x3Z compiled: x3 (BUG — dispatches to B2.g)
| case |
scenario |
interp |
compiled |
| A |
direct Derived.m(3) (static type Derived) |
103 |
103 ✓ |
| B |
base-typed b.m(3), value-type added default |
103 |
3 ✗ |
| C |
base-typed b2.g(3), reference-type added default |
x3Z |
x3 ✗ |
Compiled case B returns 3 and case C returns "x3" — exactly the results of invoking Base.m/B2.g, confirming the base-typed call binds to the base method and never reaches the override.
Root cause
Derived.m(x, y = 100) is a valid TS override of Base.m(x) because (x: number, y?: number) => number is assignable to (x: number) => number. But its CLR arity differs from the base (2 params vs 1), so the IL emitter defines it as a new (newslot) method rather than overriding the base's vtable slot. A statically-Base-typed call resolves to Base.m(double) and dispatches there.
The same-arity case is handled — ParameterTypeResolver's hierarchy-consistent widening (#705/#723/#787) unifies an override group that shares a signature (e.g. B.m(x) / D.m(x = 5), both arity 1), guarded by Override_DerivedAddsDefault_StillOverrides. It does not cover the different-arity case, where the base does not declare the added parameter at all.
Fix sketch
Emit an override shim / bridge for the base arity: Derived.m(double) marked override of Base.m(double), forwarding to Derived.m(double, double) with the omitted params filled by the default prologue. This is the "bridge methods" alternative noted in #737's fix sketch (the hierarchy-consistent-widening route shipped in #787 cannot unify signatures of different arity). Applies to instance methods generally (value-type and reference-type added defaults, and added no-default optional params — y?: number — exhibit the same dispatch gap).
Scope / discovery
Pre-existing and independent of the build-break fix it was found under (the generator-stub ParameterInfo[]/Type[] semantic merge conflict). Distinct from #723/#737/#738/#739: those are now fixed for direct calls and same-arity override groups (verified: cases A/D pass). This is the remaining different-arity override-dispatch slice. Discovered while verifying the #723/#737/#738/#739 cluster after #787.
Summary
In compiled mode, when a derived class overrides a base method by adding one or more trailing optional / default parameters (a valid TypeScript override — the signature stays assignable), a call made through a base-typed reference dispatches to the base method instead of the derived override. The derived method's added default therefore never fires, and the override is effectively invisible to base-typed call sites. The interpreter is correct.
This is not a default-parameter-firing bug: it reproduces with a reference-type default too (so the default mechanism itself is fine), and a direct call on the derived static type works. The root cause is virtual dispatch to a wider-arity override.
Repro
Derived.m(3)(static typeDerived)b.m(3), value-type added defaultb2.g(3), reference-type added defaultCompiled case B returns
3and case C returns"x3"— exactly the results of invokingBase.m/B2.g, confirming the base-typed call binds to the base method and never reaches the override.Root cause
Derived.m(x, y = 100)is a valid TS override ofBase.m(x)because(x: number, y?: number) => numberis assignable to(x: number) => number. But its CLR arity differs from the base (2 params vs 1), so the IL emitter defines it as a new (newslot) method rather than overriding the base's vtable slot. A statically-Base-typed call resolves toBase.m(double)and dispatches there.The same-arity case is handled —
ParameterTypeResolver's hierarchy-consistent widening (#705/#723/#787) unifies an override group that shares a signature (e.g.B.m(x)/D.m(x = 5), both arity 1), guarded byOverride_DerivedAddsDefault_StillOverrides. It does not cover the different-arity case, where the base does not declare the added parameter at all.Fix sketch
Emit an override shim / bridge for the base arity:
Derived.m(double)markedoverrideofBase.m(double), forwarding toDerived.m(double, double)with the omitted params filled by the default prologue. This is the "bridge methods" alternative noted in #737's fix sketch (the hierarchy-consistent-widening route shipped in #787 cannot unify signatures of different arity). Applies to instance methods generally (value-type and reference-type added defaults, and added no-default optional params —y?: number— exhibit the same dispatch gap).Scope / discovery
Pre-existing and independent of the build-break fix it was found under (the generator-stub
ParameterInfo[]/Type[]semantic merge conflict). Distinct from #723/#737/#738/#739: those are now fixed for direct calls and same-arity override groups (verified: cases A/D pass). This is the remaining different-arity override-dispatch slice. Discovered while verifying the #723/#737/#738/#739 cluster after #787.