Repository navigation
Inline *_base declaration in d.ts filesΒ #59550
Description
π Search Terms
Anonymous class, _base, base, d.ts, api-extractor, recursive
β Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
β Suggestion
Currently classes which extends anonomous classes compile to d.ts files with two declarations:
class A extends class {} {}Generates:
declare const A_base: {
new (): {};
};
declare class A extends A_base {
}My suggestion is for it to generate something like this instead:
declare class A extends ({} as {
new (): {};
}) {
}More generally replace:
declare const A_base: DEFINITION_OF_BASE_HERE;
declare class A extends A_base {
}with:
declare class A extends ({} as DEFINITION_OF_BASE_HERE) {
}π Motivating Example
When exporting classes with anonymous bases (for example classes with bases generated using functions, for example in fluid-framework's tree schema system, it would be nice if the generated d.ts file better matched the original source, so tools (like TypeScript and API-Extractor) behave more similarly to how they would if run on the source instead of on the d.ts. This would make the d.ts better serve as a concise summary of the type information of the original code.
π» Use Cases
-
What do you want to use this for?
I know of two cases this would help:- API-Extractor: it would fix [api-extractor] Extending anonymous classes errors that
_baseclass inserted by compiler is not exported.Β rushstack#4429 by removing this odd case from d.ts files. - Some recursive types compile without error when the base in inline, but not when its split into a separate variable. Sometimes this even ends up being compilation order dependent (similar to Recursive types can depend on presence and source location of unused type declarationΒ #55758 ) and can result in failing incremental builds and working clean builds. Making the d.ts declare it inline when the original source files does helps make the d.ts more aligned with the non-d.ts type checking for recursive types avoiding introducing additional such issues specific to the d.ts. I haven't extracted a minimal repro for this, but there are some not so minimal examples (and a workaround) in tree: Add Recursive ArrayNode export workaroundΒ FluidFramework#22122.
- API-Extractor: it would fix [api-extractor] Extending anonymous classes errors that
-
What shortcomings exist with current approaches?
Currently the behavior is confusing since most TS developers don't think about the differences between the d.ts files and the original source, so having to do workarounds to make them stay aligned (like manually exporting the base to make API-Extractor happy, or ensuring that if the base is not inline, that the code still type checked for the recursive case) is confusing and unintuitive. -
What workarounds are you using in the meantime?
For API extractor, manually export the base type under a different name.
For the recursive type issue, export carefully crafted usage before the declaration which happens to cause it to compile with the split declaration.
Activity
CraigMacomber commentedon Nov 8, 2024
I hit this again. It makes it really annoying for users of Fluid Framework's SharedTree to use API-Extractor since doing so requires really annoying workarounds to export schema.
A schema definition like
export class JsonArray extends sf.arrayRecursive("array", JsonUnion) {}has to becomeHaving to do this for every shared tree schema makes a real mess schema files and the package API surface.
It also exposes customers of SharedTree and API-Extractor to an implementation detail of d.ts generation which they really shouldn't have to know, but if they don't know, they will have a hard time knowing what to do about an error like
(ae-forgotten-export) The symbol "JsonArray_base" needs to be exported by the entry point alpha.d.tswhen that's a symbol they never defined. THis is annoying for me who already knows exactly that's going on and how to work around it: I imagine for someone with no idea about the _base types in d.ts files, it would be quite confusing.octogonz commentedon Nov 8, 2024
Also, in general it is difficult to detect these
_basedeclarations, as they can have other names such as_base_1. For example:example.ts
example.d.ts
The fundamental challenge is that, by design, API Extractor processes .d.ts files and does not consider their .ts inputs. I agree that, this case, emitting an algebraic type might make the .d.ts file actually easier to interpret.
CraigMacomber commentedon Jan 30, 2025
I hit this again. I'd love to see my suggested fix implemented: I'm not aware of any downsides to it.
I think the code that needs to change is
TypeScript/src/compiler/transformers/declarations.ts
Lines 1659 to 1703 in 739d729
I suspect that entire if/else could be removed and use my suggested format in both cases, or just replace the
TypeScript/src/compiler/transformers/declarations.ts
Lines 1660 to 1691 in 739d729
CraigMacomber commentedon Jan 31, 2025
I have created #61087 with an implementation of this change.
CraigMacomber commentedon Feb 7, 2025
The TypeScript team suggested I make a PR to implement my suggestion if I wanted progress to be made on this issue. That is the PR linked above. They then reviewed my proposal and communicated to me indirectly that my proposal exploits a parser bug since expressions are not permitted in ambient contexts (my entire suggestion was to cram an expression in an ambient context, so this makes sense).
This makes it difficult to find a solution since existing versions of TypeScript give an error "Expression expected.(1109)" if you provide something other than an expression in that location.
The requirement for existing TypeScript versions that you provide an expression for the extends clause (in
declarestatements and otherwise), and the desire that declare statements contain things that are not expressions means that the only valid approach is to provide something that parses validly as an expression, but when used in adeclare classextends context could be parsed by future versions of TypeScript something other than an expression.To me this seems like future versions of TypeScript would have to make a breaking change to the parser/grammar to redefine what is allowed in
declare classextends clauses.To make my change to be compatible with this future breaking change to TypeScript grammar, I'd like to know what would be accepted in this context by these theoretical future TypeScript versions?
My understanding is that in ambient contexts, we allow
typeof something.In declare class extends clauses the relevant type is the type form of
typeofapplied to the expression.Thus am I correct in assuming that if future versions of TypeScript want to make a breaking change to what's allowed in extends clauses of
declare class, then it would likely be to apply the same restrictions thattypeofhas?Assuming the above is correct, it seems like the design of the
declare classsyntax forextendswas a mistake, and probably should have taken in the type of the base class (explicitly using typeof in common cases) which would make it possible to use a type directly there when desired, which is the case here. Fixing that however is impractical as it would be a breaking change.There is an interesting workaround for that problem however, which is also backwards compatible with existing versions of TypeScript: you allow syntaxes for declare class:
declare class <Identifier> extends <thing valid to give to typeof> {...}declare class <Identifier> extends ({} as <TYPE> {...}This makes it possible to give a types directly to the extends clause without using an expression (in compilers with the new grammer), while keeping most existing code still working (provides expressions compatible with typeof). This new syntax also just happens to work with existing compilers (they parse it as an expression), so generating it wouldn't cause compatibility issues.
Changing the parser to stop supporting expressions in this context is still a breaking change, but the above approach could allow my workaround to continue to function with or without that breaking change ensuring forwards and backwards compatibility for my suggestion here, even if other expressions are banned.