Skip to content

Generated d.ts includes implicit any for recursive types #55832

Open
@CraigMacomber

Description

@CraigMacomber

πŸ”Ž Search Terms

recursive any noImplicitAny d.ts

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried (3.3 through 5.2), and I reviewed the FAQ for entries about anything related to this.

⏯ Playground Link

playground link

πŸ’» Code

const a = () => a;

πŸ™ Actual behavior

Any is implicitly introduced in the d.ts:

declare const a: () => any;

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 if noImplicitAny is enabled.
In this case, the below code generation for the d.ts would work:

declare const a: () => typeof a;

Additional information about the issue

No response

Activity

fatcerberus

fatcerberus commented on Sep 22, 2023

@fatcerberus

I’m more surprised this doesn’t have a circularity error, since a genuinely refers to itself in its own initializer and the fully expanded type of a is therefore () => () => () => ... ad infinitum (I don’t think TS generally infers typeof types)

CraigMacomber

CraigMacomber commented on Sep 22, 2023

@CraigMacomber
Author

I’m more surprised this doesn’t have a circularity error, since a genuinely refers to itself in its own initializer and the fully expanded type of a is therefore () => () => () => ... ad infinitum (I don’t think TS generally infers typeof types)

I just found a workaround which actually both avoids the need for inferring a typeof type in the d.ts, which for some reason also generates a typeof type 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 uses typeof and adds a MySchema_base constant that does not exist in the JS.

declare const MySchema_base: Schema<() => typeof MySchema>;
declare class MySchema extends MySchema_base {
}

playground link

fatcerberus

fatcerberus commented on Sep 23, 2023

@fatcerberus

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 any and in fact does something I didn't even know was possible:

image

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. a is not treated as any locally; you can't assign it to a number or string for example, but can write any number of pairs of parentheses after a() and it still typechecks. Very odd.

CraigMacomber

CraigMacomber commented on Sep 26, 2023

@CraigMacomber
Author

@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.

fatcerberus

fatcerberus commented on Sep 26, 2023

@fatcerberus

@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 any in the sense that noImplicitAny is 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

CraigMacomber commented on Sep 26, 2023

@CraigMacomber
Author

@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 any in the sense that noImplicitAny is 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 get any instead. 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.

added
Possible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some cases
on Oct 3, 2023
added this to the Backlog milestone on Oct 3, 2023
RyanCavanaugh

RyanCavanaugh commented on Oct 3, 2023

@RyanCavanaugh
Member

It seems very reasonable to emit a noImplicitAny error when this happens specifically during declaration emit.

andrewbranch

andrewbranch commented on Nov 21, 2023

@andrewbranch
Member

#56479 should be included as a test case for this.

jakebailey

jakebailey commented on Nov 21, 2023

@jakebailey
Member

It seems very reasonable to emit a noImplicitAny error when this happens specifically during declaration emit.

Further, it'd be awesome to emit some sort of error any time dts emit produces an any out of nowhere, but when we were talking in the isolatedDeclaration meeting, such a thing seemed quite challenging given this is all pretty deep in typeToTypeString. But, maybe some sort of flag or something would be sufficient...

added a commit that references this issue on Nov 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Help WantedYou can do thisPossible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some cases

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @andrewbranch@fatcerberus@jakebailey@RyanCavanaugh@CraigMacomber

        Issue actions

          Generated d.ts includes implicit any for recursive types Β· Issue #55832 Β· microsoft/TypeScript