From 13c6d1f814b7a2dff0593cafa050ce4b4d42c616 Mon Sep 17 00:00:00 2001 From: Stuart Robinson Date: Sun, 8 Mar 2026 09:19:48 +0700 Subject: [PATCH] Fix silentNeverType leak through keyof in return type inference (#62824) When a generic function call is nested inside another generic call, the inner call's type parameters receive silentNeverType as a placeholder during the outer call's inference pass. The keyof operator applied to silentNeverType previously resolved to string | number | symbol via the TypeFlags.Never branch in getIndexType, stripping the NonInferrableType flag and allowing the internal sentinel to leak into user-facing error messages as an unexpected "never". Add an identity check for silentNeverType in getIndexType, matching the existing pattern used for wildcardType. This preserves the sentinel through keyof operations so that NonInferrableType propagation remains intact during mapped type instantiation. Co-Authored-By: Claude Opus 4.6 --- src/compiler/checker.ts | 1 + .../silentNeverTypeKeyofLeak.symbols | 143 ++++++++++++++++++ .../reference/silentNeverTypeKeyofLeak.types | 132 ++++++++++++++++ .../compiler/silentNeverTypeKeyofLeak.ts | 55 +++++++ 4 files changed, 331 insertions(+) create mode 100644 tests/baselines/reference/silentNeverTypeKeyofLeak.symbols create mode 100644 tests/baselines/reference/silentNeverTypeKeyofLeak.types create mode 100644 tests/cases/compiler/silentNeverTypeKeyofLeak.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0567712f11da3..dec3d6542422f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -18989,6 +18989,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { type.flags & TypeFlags.Intersection ? getUnionType(map((type as IntersectionType).types, t => getIndexType(t, indexFlags))) : getObjectFlags(type) & ObjectFlags.Mapped ? getIndexTypeForMappedType(type as MappedType, indexFlags) : type === wildcardType ? wildcardType : + type === silentNeverType ? silentNeverType : type.flags & TypeFlags.Unknown ? neverType : type.flags & (TypeFlags.Any | TypeFlags.Never) ? stringNumberSymbolType : getLiteralTypeFromProperties(type, (indexFlags & IndexFlags.NoIndexSignatures ? TypeFlags.StringLiteral : TypeFlags.StringLike) | (indexFlags & IndexFlags.StringsOnly ? 0 : TypeFlags.NumberLike | TypeFlags.ESSymbolLike), indexFlags === IndexFlags.None); diff --git a/tests/baselines/reference/silentNeverTypeKeyofLeak.symbols b/tests/baselines/reference/silentNeverTypeKeyofLeak.symbols new file mode 100644 index 0000000000000..5f21c2420b113 --- /dev/null +++ b/tests/baselines/reference/silentNeverTypeKeyofLeak.symbols @@ -0,0 +1,143 @@ +//// [tests/cases/compiler/silentNeverTypeKeyofLeak.ts] //// + +=== silentNeverTypeKeyofLeak.ts === +// Repro from #62824 + +// Minimal repro: silentNeverType leaks through keyof in mapped types +// when a nested generic call has no inference candidates during the +// outer call's inference pass. + +type Fn = (arg: T) => void; +>Fn : Symbol(Fn, Decl(silentNeverTypeKeyofLeak.ts, 0, 0)) +>T : Symbol(T, Decl(silentNeverTypeKeyofLeak.ts, 6, 8)) +>arg : Symbol(arg, Decl(silentNeverTypeKeyofLeak.ts, 6, 14)) +>T : Symbol(T, Decl(silentNeverTypeKeyofLeak.ts, 6, 8)) + +declare function fn1(): Fn; +>fn1 : Symbol(fn1, Decl(silentNeverTypeKeyofLeak.ts, 6, 30)) +>T : Symbol(T, Decl(silentNeverTypeKeyofLeak.ts, 8, 21)) +>Fn : Symbol(Fn, Decl(silentNeverTypeKeyofLeak.ts, 0, 0)) +>T : Symbol(T, Decl(silentNeverTypeKeyofLeak.ts, 8, 21)) + +declare function fn2( +>fn2 : Symbol(fn2, Decl(silentNeverTypeKeyofLeak.ts, 8, 33)) +>T : Symbol(T, Decl(silentNeverTypeKeyofLeak.ts, 10, 21)) + + ac: Fn<{ +>ac : Symbol(ac, Decl(silentNeverTypeKeyofLeak.ts, 10, 24)) +>Fn : Symbol(Fn, Decl(silentNeverTypeKeyofLeak.ts, 0, 0)) + + [K in keyof T & string]: T[K]; +>K : Symbol(K, Decl(silentNeverTypeKeyofLeak.ts, 12, 5)) +>T : Symbol(T, Decl(silentNeverTypeKeyofLeak.ts, 10, 21)) +>T : Symbol(T, Decl(silentNeverTypeKeyofLeak.ts, 10, 21)) +>K : Symbol(K, Decl(silentNeverTypeKeyofLeak.ts, 12, 5)) + + }>, +): void; + +fn2(fn1()); +>fn2 : Symbol(fn2, Decl(silentNeverTypeKeyofLeak.ts, 8, 33)) +>fn1 : Symbol(fn1, Decl(silentNeverTypeKeyofLeak.ts, 6, 30)) + +// Shorter repro from issue +type Values = T[keyof T]; +>Values : Symbol(Values, Decl(silentNeverTypeKeyofLeak.ts, 16, 11)) +>T : Symbol(T, Decl(silentNeverTypeKeyofLeak.ts, 19, 12)) +>T : Symbol(T, Decl(silentNeverTypeKeyofLeak.ts, 19, 12)) +>T : Symbol(T, Decl(silentNeverTypeKeyofLeak.ts, 19, 12)) + +interface ParameterizedObject { +>ParameterizedObject : Symbol(ParameterizedObject, Decl(silentNeverTypeKeyofLeak.ts, 19, 28)) + + type: string; +>type : Symbol(ParameterizedObject.type, Decl(silentNeverTypeKeyofLeak.ts, 21, 31)) + + params?: unknown; +>params : Symbol(ParameterizedObject.params, Decl(silentNeverTypeKeyofLeak.ts, 22, 15)) +} + +type ActionFunction = { +>ActionFunction : Symbol(ActionFunction, Decl(silentNeverTypeKeyofLeak.ts, 24, 1)) +>TParams : Symbol(TParams, Decl(silentNeverTypeKeyofLeak.ts, 26, 20)) +>TAction : Symbol(TAction, Decl(silentNeverTypeKeyofLeak.ts, 26, 28)) +>ParameterizedObject : Symbol(ParameterizedObject, Decl(silentNeverTypeKeyofLeak.ts, 19, 28)) + + (params: TParams): void; +>params : Symbol(params, Decl(silentNeverTypeKeyofLeak.ts, 27, 3)) +>TParams : Symbol(TParams, Decl(silentNeverTypeKeyofLeak.ts, 26, 20)) + + _out_TAction?: TAction; +>_out_TAction : Symbol(_out_TAction, Decl(silentNeverTypeKeyofLeak.ts, 27, 26)) +>TAction : Symbol(TAction, Decl(silentNeverTypeKeyofLeak.ts, 26, 28)) + +}; + +type ToParameterizedObject = Values<{ +>ToParameterizedObject : Symbol(ToParameterizedObject, Decl(silentNeverTypeKeyofLeak.ts, 29, 2)) +>TParameterizedMap : Symbol(TParameterizedMap, Decl(silentNeverTypeKeyofLeak.ts, 31, 27)) +>Values : Symbol(Values, Decl(silentNeverTypeKeyofLeak.ts, 16, 11)) + + [K in keyof TParameterizedMap & string]: { +>K : Symbol(K, Decl(silentNeverTypeKeyofLeak.ts, 32, 3)) +>TParameterizedMap : Symbol(TParameterizedMap, Decl(silentNeverTypeKeyofLeak.ts, 31, 27)) + + type: K; +>type : Symbol(type, Decl(silentNeverTypeKeyofLeak.ts, 32, 44)) +>K : Symbol(K, Decl(silentNeverTypeKeyofLeak.ts, 32, 3)) + + params: TParameterizedMap[K]; +>params : Symbol(params, Decl(silentNeverTypeKeyofLeak.ts, 33, 12)) +>TParameterizedMap : Symbol(TParameterizedMap, Decl(silentNeverTypeKeyofLeak.ts, 31, 27)) +>K : Symbol(K, Decl(silentNeverTypeKeyofLeak.ts, 32, 3)) + + }; +}>; + +declare function enqueueActions( +>enqueueActions : Symbol(enqueueActions, Decl(silentNeverTypeKeyofLeak.ts, 36, 3)) +>TParams : Symbol(TParams, Decl(silentNeverTypeKeyofLeak.ts, 38, 32)) +>TAction : Symbol(TAction, Decl(silentNeverTypeKeyofLeak.ts, 38, 40)) +>ParameterizedObject : Symbol(ParameterizedObject, Decl(silentNeverTypeKeyofLeak.ts, 19, 28)) + + collect: (params: TParams) => void, +>collect : Symbol(collect, Decl(silentNeverTypeKeyofLeak.ts, 38, 78)) +>params : Symbol(params, Decl(silentNeverTypeKeyofLeak.ts, 39, 12)) +>TParams : Symbol(TParams, Decl(silentNeverTypeKeyofLeak.ts, 38, 32)) + +): ActionFunction; +>ActionFunction : Symbol(ActionFunction, Decl(silentNeverTypeKeyofLeak.ts, 24, 1)) +>TParams : Symbol(TParams, Decl(silentNeverTypeKeyofLeak.ts, 38, 32)) +>TAction : Symbol(TAction, Decl(silentNeverTypeKeyofLeak.ts, 38, 40)) + +declare function setup(actions: { +>setup : Symbol(setup, Decl(silentNeverTypeKeyofLeak.ts, 40, 36)) +>TActions : Symbol(TActions, Decl(silentNeverTypeKeyofLeak.ts, 42, 23)) +>actions : Symbol(actions, Decl(silentNeverTypeKeyofLeak.ts, 42, 33)) + + [K in keyof TActions]: ActionFunction< +>K : Symbol(K, Decl(silentNeverTypeKeyofLeak.ts, 43, 3)) +>TActions : Symbol(TActions, Decl(silentNeverTypeKeyofLeak.ts, 42, 23)) +>ActionFunction : Symbol(ActionFunction, Decl(silentNeverTypeKeyofLeak.ts, 24, 1)) + + TActions[K], +>TActions : Symbol(TActions, Decl(silentNeverTypeKeyofLeak.ts, 42, 23)) +>K : Symbol(K, Decl(silentNeverTypeKeyofLeak.ts, 43, 3)) + + ToParameterizedObject +>ToParameterizedObject : Symbol(ToParameterizedObject, Decl(silentNeverTypeKeyofLeak.ts, 29, 2)) +>TActions : Symbol(TActions, Decl(silentNeverTypeKeyofLeak.ts, 42, 23)) + + >; +}): void; + +setup({ +>setup : Symbol(setup, Decl(silentNeverTypeKeyofLeak.ts, 40, 36)) + + doStuff: enqueueActions((params: number) => {}), +>doStuff : Symbol(doStuff, Decl(silentNeverTypeKeyofLeak.ts, 49, 7)) +>enqueueActions : Symbol(enqueueActions, Decl(silentNeverTypeKeyofLeak.ts, 36, 3)) +>params : Symbol(params, Decl(silentNeverTypeKeyofLeak.ts, 50, 27)) + +}); + diff --git a/tests/baselines/reference/silentNeverTypeKeyofLeak.types b/tests/baselines/reference/silentNeverTypeKeyofLeak.types new file mode 100644 index 0000000000000..d952cdc627156 --- /dev/null +++ b/tests/baselines/reference/silentNeverTypeKeyofLeak.types @@ -0,0 +1,132 @@ +//// [tests/cases/compiler/silentNeverTypeKeyofLeak.ts] //// + +=== silentNeverTypeKeyofLeak.ts === +// Repro from #62824 + +// Minimal repro: silentNeverType leaks through keyof in mapped types +// when a nested generic call has no inference candidates during the +// outer call's inference pass. + +type Fn = (arg: T) => void; +>Fn : Fn +> : ^^^^^ +>arg : T +> : ^ + +declare function fn1(): Fn; +>fn1 : () => Fn +> : ^ ^^^^^^^ + +declare function fn2( +>fn2 : (ac: Fn<{ [K in keyof T & string]: T[K]; }>) => void +> : ^ ^^ ^^ ^^^^^ + + ac: Fn<{ +>ac : Fn<{ [K in keyof T & string]: T[K]; }> +> : ^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + [K in keyof T & string]: T[K]; + }>, +): void; + +fn2(fn1()); +>fn2(fn1()) : void +> : ^^^^ +>fn2 : (ac: Fn<{ [K in keyof T & string]: T[K]; }>) => void +> : ^ ^^ ^^ ^^^^^ +>fn1() : Fn<{}> +> : ^^^^^^ +>fn1 : () => Fn +> : ^ ^^^^^^^ + +// Shorter repro from issue +type Values = T[keyof T]; +>Values : Values +> : ^^^^^^^^^ + +interface ParameterizedObject { + type: string; +>type : string +> : ^^^^^^ + + params?: unknown; +>params : unknown +> : ^^^^^^^ +} + +type ActionFunction = { +>ActionFunction : ActionFunction +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + (params: TParams): void; +>params : TParams +> : ^^^^^^^ + + _out_TAction?: TAction; +>_out_TAction : TAction | undefined +> : ^^^^^^^^^^^^^^^^^^^ + +}; + +type ToParameterizedObject = Values<{ +>ToParameterizedObject : ToParameterizedObject +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + [K in keyof TParameterizedMap & string]: { + type: K; +>type : K +> : ^ + + params: TParameterizedMap[K]; +>params : TParameterizedMap[K] +> : ^^^^^^^^^^^^^^^^^^^^ + + }; +}>; + +declare function enqueueActions( +>enqueueActions : (collect: (params: TParams) => void) => ActionFunction +> : ^ ^^ ^^^^^^^^^ ^^ ^^ ^^^^^ + + collect: (params: TParams) => void, +>collect : (params: TParams) => void +> : ^ ^^ ^^^^^ +>params : TParams +> : ^^^^^^^ + +): ActionFunction; + +declare function setup(actions: { +>setup : (actions: { [K in keyof TActions]: ActionFunction>; }) => void +> : ^ ^^ ^^ ^^^^^ +>actions : { [K in keyof TActions]: ActionFunction>; } +> : ^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + [K in keyof TActions]: ActionFunction< + TActions[K], + ToParameterizedObject + >; +}): void; + +setup({ +>setup({ doStuff: enqueueActions((params: number) => {}),}) : void +> : ^^^^ +>setup : (actions: { [K in keyof TActions]: ActionFunction>; }) => void +> : ^ ^^ ^^ ^^^^^ +>{ doStuff: enqueueActions((params: number) => {}),} : { doStuff: ActionFunction; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + doStuff: enqueueActions((params: number) => {}), +>doStuff : ActionFunction +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>enqueueActions((params: number) => {}) : ActionFunction +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>enqueueActions : (collect: (params: TParams) => void) => ActionFunction +> : ^ ^^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>(params: number) => {} : (params: number) => void +> : ^ ^^ ^^^^^^^^^ +>params : number +> : ^^^^^^ + +}); + diff --git a/tests/cases/compiler/silentNeverTypeKeyofLeak.ts b/tests/cases/compiler/silentNeverTypeKeyofLeak.ts new file mode 100644 index 0000000000000..2d41dcb2db1b1 --- /dev/null +++ b/tests/cases/compiler/silentNeverTypeKeyofLeak.ts @@ -0,0 +1,55 @@ +// @strict: true +// @noEmit: true + +// Repro from #62824 + +// Minimal repro: silentNeverType leaks through keyof in mapped types +// when a nested generic call has no inference candidates during the +// outer call's inference pass. + +type Fn = (arg: T) => void; + +declare function fn1(): Fn; + +declare function fn2( + ac: Fn<{ + [K in keyof T & string]: T[K]; + }>, +): void; + +fn2(fn1()); + +// Shorter repro from issue +type Values = T[keyof T]; + +interface ParameterizedObject { + type: string; + params?: unknown; +} + +type ActionFunction = { + (params: TParams): void; + _out_TAction?: TAction; +}; + +type ToParameterizedObject = Values<{ + [K in keyof TParameterizedMap & string]: { + type: K; + params: TParameterizedMap[K]; + }; +}>; + +declare function enqueueActions( + collect: (params: TParams) => void, +): ActionFunction; + +declare function setup(actions: { + [K in keyof TActions]: ActionFunction< + TActions[K], + ToParameterizedObject + >; +}): void; + +setup({ + doStuff: enqueueActions((params: number) => {}), +});