Repository navigation
Generated d.ts includes implicit any for recursive typesΒ #55832
Description
Activity
Iβm more surprised this doesnβt have a circularity error, since
agenuinely refers to itself in its own initializer and the fully expanded type ofais therefore() => () => () => ...ad infinitum (I donβt think TS generally inferstypeoftypes)CraigMacomber commented
on Sep 22, 2023 AuthorMore actionsIβm more surprised this doesnβt have a circularity error, since
agenuinely refers to itself in its own initializer and the fully expanded type ofais therefore() => () => () => ...ad infinitum (I donβt think TS generally inferstypeoftypes)I just found a workaround which actually both avoids the need for inferring a
typeoftype in the d.ts, which for some reason also generates atypeoftype in the d.ts. Well, two redundant workarounds for the price of one is still a workaround :)/** * Interface which carries the runtime and compile type data (from the generic type paramater) in a member. * This is also a constructor so that instances of it can be extended as classes. * Using classes in this way allows introducing a named type and a named value at the same time, helping keep the runtime and compile time information together and easy to refer to un a uniform way. * Addationally, this works around https://github.com/microsoft/TypeScript/issues/55832 which causes similar patterns with less explicit types to infer "any" in the d.ts file. */ interface Schema<ChildSchema> extends SchemaData<ChildSchema> { // We don't actually ever call this constructor, // but having it allows using classes to introduce runtime and named types at the same time, working around https://github.com/microsoft/TypeScript/issues/55832 new (dummy: never): SchemaData<ChildSchema>; } /** * Used as both a class and an interface. * Helper for declaring Schema. */ class SchemaData<T> { constructor(public readonly data: T) {} } /** * Builder for the Schema interface. */ function build<T>(childType: T): Schema<T> { return class extends SchemaData<T> { static readonly data = childType; constructor() { super(childType); } } } // example use class MySchema extends build(() => MySchema) {} // Strong recursive typing works correctly: const child: MySchema = MySchema.data().data().data();
The d.ts does not produce
any, instead it usestypeofand adds aMySchema_baseconstant that does not exist in the JS.declare const MySchema_base: Schema<() => typeof MySchema>; declare class MySchema extends MySchema_base { }
Looking at this again, I think there might be a genuine bug here, just not the one presupposed in the OP; this isn't actually an implicit
anyand in fact does something I didn't even know was possible:It seems like TypeScript knows the type is infinitely expanding, and that's totally fine. The problem only arises when it has to generate a declaration for it, at which point it apparently falls over and defaults to
any.ais not treated asanylocally; you can't assign it to anumberorstringfor example, but can write any number of pairs of parentheses aftera()and it still typechecks. Very odd.CraigMacomber commented
on Sep 26, 2023 AuthorMore actionsBruce Pascoe (@fatcerberus) That is the bug I was attempting to report. Everything is correct locally, but the generated d.ts contains "any" which was not explicitly in the source anywhere. I'll update it to clarify that it works locally in the description.
Reacted by Bruce Pascoe and Ryan CavanaughCraig Macomber (Microsoft) (@CraigMacomber) I thought so, but I wanted to be sure after you said this:
Either generate something without implicitly introducing any, or generate an error if noImplicitAny is enabled.
which isnβt really relevant IMO since thereβs no implicit
anyin the sense thatnoImplicitAnyis meant to guard against (i.e. type inference failure introducing an implicit any); itβs seemingly just the declaration emitter falling over on something whose type was already correctly inferred.CraigMacomber commented
on Sep 26, 2023 AuthorMore actionsCraig Macomber (Microsoft) (@CraigMacomber) I thought so, but I wanted to be sure after you said this:
Either generate something without implicitly introducing any, or generate an error if noImplicitAny is enabled.
which isnβt really relevant IMO since thereβs no implicit
anyin the sense thatnoImplicitAnyis meant to guard against (i.e. type inference failure introducing an implicit any); itβs seemingly just the declaration emitter falling over on something whose type was already correctly inferred.Its implicitly introducing any in the API for my package via the d.ts file.
TypeScript has a known design decision/limitation that the API of .dts files doesn't match the local type checking exactly, so the fact that they are different is not always a bug.
Its also a known design limitation of TypeScript that sometimes recursive types don't work arbitrarily, and you getanyinstead. This is also not a bug.
Thus I'm highlighting that the produced type is violating the noImplicitAny rule, which I think makes it a bug.- addedPossible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some casesThe current behavior isn't wrong, but it's possible to see that it might be better in some casesHelp WantedYou can do thisYou can do this
on Oct 3, 2023 RyanCavanaugh commented
on Oct 3, 2023 MemberMore actionsIt seems very reasonable to emit a
noImplicitAnyerror when this happens specifically during declaration emit.Reacted by Craig Macomber (Microsoft)andrewbranch commented
on Nov 21, 2023 MemberMore actions#56479 should be included as a test case for this.
It seems very reasonable to emit a
noImplicitAnyerror when this happens specifically during declaration emit.Further, it'd be awesome to emit some sort of error any time dts emit produces an
anyout of nowhere, but when we were talking in theisolatedDeclarationmeeting, such a thing seemed quite challenging given this is all pretty deep intypeToTypeString. But, maybe some sort of flag or something would be sufficient...- added a commit that references this issue
on Nov 28, 2023 - addedDomain: Declaration EmitThe issue relates to the emission of d.ts filesThe issue relates to the emission of d.ts files
on Oct 16, 2025

π Search Terms
recursive any noImplicitAny d.ts
π Version & Regression Information
β― Playground Link
playground link
π» Code
π Actual behavior
Any is implicitly introduced in the d.ts:
Note that the local type checking (not using the d.ts) correctly handles this recursive type without introducing "any", and builds fine with noImplicitAny enabled.
π Expected behavior
Either generate something without implicitly introducing
any, or generate an error ifnoImplicitAnyis enabled.In this case, the below code generation for the d.ts would work:
Additional information about the issue
No response