-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Isolated declarations #53463
Isolated declarations #53463
Conversation
This PR doesn't have any linked issues. Please open an issue that references this PR. From there we can discuss and prioritise. |
The TypeScript team hasn't accepted the linked issue #47947. If you can get it accepted, this PR will have a better chance of being reviewed. |
export class Test {
x // error under isolated declarations
private y = 0; // no error, private field types are not serialized in declarations
#z = 1;// no error, fields is not present in declarations
constructor(x: number) {
this.x = 1;
}
get a() { // error under isolated declarations
return 1;
}
set a(value) { // error under isolated declarations
this.x = 1;
}
} you cannot emit private or #private fields, but emitting them is easy. they're emitted as any; this is what ts emit today: export declare class Test {
#private;
x: number;
private y;
constructor(x: number);
get a(): number;
set a(value: number);
} the private/#private declaration is used to make those classes become a nominal typed class. |
@typescript-bot pack this |
Heya @DanielRosenwasser, I've started to run the tarball bundle task on this PR at ed568e2. You can monitor the build here. |
Wow that is one massive wall of text in the OP |
This is super exciting. I tested I tried to get an idea about the impact of the DX Improvement Opportunities. Sadly, those 36% are too many targets to triage manually. I expect that D1 and D2 could have the largest impact. D3 - D8 feel slightly more niche, though I don’t have data to back that up. I did some crude regex searching on the error diagnostics and found that:
This suggests that D2 could remove >=13% of the errors, especially if it also accounted for arrays and objects. Separately, I noticed that ~4% of the errors used Another data point: Last year we asked TypeScript developers at Google how they feel about adding explicitly type annotations on exported symbols. Of ~160 responses, 40% liked it, 40% disliked it and 20% didn’t care. From comments we gathered that the dislike often stemmed from having to write what feels like trivial type annotations. D1 or D2 could hopefully address those concerns. Again, this is really exciting. Thank you for working on this! |
What happens to DOM declarations? Or are you excusing all |
It feels this compiler flag is targeting pretty narrow corner case. And if it's enabled unintentionally creates confused set of syntactic rules that would produce a deluge of inexplicable compiler errors. Can the behavior be achieved by wrapping and invoking TS APIs instead of introducing new mode into the compiler itself? |
I'm not sure what you mean. Declaration files do not need to be emitted again (and they are fully typed anyway). Symbols defined in the lib files will be subject to the same rules. For example
This change is targeted for monorepos. There are several monorepo tools that could take advantage of this change today. Also in the future typescript composite projects could take advantage of this. So there are users out there that can benefit from this, albeit in large multi project workspaces. Improving the workflow in these scenarios is not without merit IMO.
I don't find this a compelling reason. If you enabled it after the code is written and you get a deluge of errors, you will quickly find the last ts config change, or you will be able to search for the errors and quickly find that they are related to this flag. (Also I don't think it is very common for people to accidentally enable compiler flags)
The idea is to enable other tools to produce declaration files. If we bring the type checker into it (ie use the compiler API) the external tools will be as slow as TS since you need to type check (alt least in part) before you can synthesize types. If we proceeded without the type checker some changes need to be made to how code is written as to not require types that can't easily be created without full type info. Without a common understanding of what those changes are it is difficult for other tools to make investments in this space. It is also difficult for users to adopt such tools as their restrictions might be different (and so swapping them out is difficult, leading to concerns about if a project is abandoned are we stuck with an unsupported tool) |
a8bb6e2
to
ae679bf
Compare
I think it's okay to infer the types of simple literals (number, string, boolean, null, undefined). However I don't think you really want to be inferring types for all object cases or any array cases. ObjectsPerformance(From my understanding) Most object cases would produce better TS perf/memory use to use a named type as opposed to an anonymous one. Error MessagesNamed types also produce better error messages. For example (I know this an array base but it serves as a good example of error message improvements): let arr = [{ foo: 1 }, { bar: "" }];
arr.push({ baz: true });
// Argument of type '{ baz: boolean; }' is not assignable to parameter of type '{ foo: number; bar?: undefined; } | { bar: string; foo?: undefined; }'.
// Object literal may only specify known properties, and 'baz' does not exist in type '{ foo: number; bar?: undefined; } | { bar: string; foo?: undefined; }'.
type T = { foo: number; bar?: undefined; } | { bar: string; foo?: undefined;};
let arr2: T[] = arr;
arr2.push({ baz: true });
// Argument of type '{ baz: boolean; }' is not assignable to parameter of type 'T'.
// Object literal may only specify known properties, and 'baz' does not exist in type 'T'. Ambiguous / untyped propertiesUnlike exported variables with simple values, non-readonly exported objects are inherently mutable from other modules. The simple literal cases are fine to infer (literal types become their parent type) and functions can be enforced to be typed as if they were directly exported (eg must have parameter types and a return type), but what should be inferred when the value is something like If you infer the literal type then you create a scenario where a downstream consumer may be unable to use the object as intended. export const x = { prop: null };
x.prop = 1; // Type '1' is not assignable to type 'null' One could definitely argue that this is fine because that's how TS currently works - though I would personally prefer that this option forces you to be less ambiguous here and either There's ofc the array case as well (see below). ArraysArrays are much the same as above, though things can be more ambiguous, IMO. In OP you describe this case: let arr = [{ foo: 1 }, { bar: "" }] Your example types this code as this: let arr: ({ foo: number; bar?: undefined; } | { bar: string; foo?: undefined;})[]; Which is how TS infers the type. However one could also argue that the type could instead be the stricter and safer let arr: ({ foo: number } | { bar: string })[]; The difference being that the former allows One could also argue that it was intended to be the wider type let arr: ({ foo?: number; bar?: string })[]; There's a lot of ambiguity here and allowing inference for these cases would mean that tools would ultimately just seek to align with what TS infers (which is a complex thing to replicate exactly). I would personally propose that array literals are treated the same as any function/constructor call - which is to say they need to be explicitly annotated. |
Personally I don't think using IMO it'd be better to skip the middle-man and enforce an explicit type annotation. I think if the user wants to explicitly use It also complicates the translation algorithm somewhat because you would need to use scope analysis to follow the declaration of the variable to determine if it's correctly typed. For example imagine: let a = Math.min(1, 1);
export const b = a; A transpiler can't just do Additionally this would result in excess code being required in many cases. For example if the above example was correctly annotated you would generate: declare let a: number;
export const b: typeof a; instead of the more concise and simpler: export const b: number; One could argue that excess code is fine and a minification step could be implemented to reduce cruft - but I personally think you'd just want to be strict and simple by default. |
I don't think it's safe to do this as it would allow declarations that would be errors: declare const A: string;
export declare class Foo {
[A](): void;
// A computed property name in a method overload must refer to an expression whose type is a literal type or a 'unique symbol' type.
[A](): string;
// A computed property name in a method overload must refer to an expression whose type is a literal type or a 'unique symbol' type.
} This would also be impossible to handle in isolation if, for example, There's also a weird case with computed name methods in that currently TS will not error, but it will omit the property from its declaration entirely: declare const A: string;
export class Foo {
[A](): number {
return 1;
}
}
export class Bar {
[A as "A"](): number {
return 1;
}
}
declare const B: 'B';
export class Baz {
[B](): number {
return 1;
}
}
/////// Current .d.ts output ///////
export declare class Foo {
}
export declare class Bar {
}
declare const B: 'B';
export declare class Baz {
[B](): number;
}
export {}; IDK if this is a bug in TS or not considering that if the member is instead a property it will explicitly error. |
I read through the comment history here and have read through half of emitBinder.ts. So far the code seems intended as a separate, smaller, faster re-implementation of everything but the parser. If so, why is this inside tsc? The original post mentions plans to split this into something separate. Is that still planned? Putting a new implementation of Typescript next to the existing one is both slow and confusing for future development. |
@sandersn We discussed this and the reasons we decided to keep this in TS were:
The whole implementation is about 1.5K likes with the binder being the biggest part of it at about 900 lines, so it's not a huge amount of extra code. |
Signed-off-by: Titian Cernicova-Dragomir <[email protected]>
…tale-reasons Remove stale diff reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments about emitBinder.ts, only about half way through with it so far.
excludes: SymbolFlags; | ||
} | ||
const syntaxKindToSymbolMap = { | ||
[SyntaxKind.TypeParameter]: { includes: SymbolFlags.TypeParameter, excludes: SymbolFlags.TypeParameterExcludes }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this the way the binder does it?
later: no, it's defined in code, by calling declareSymbol with particular arguments. So I guess this way is more organised.
/** | ||
* Assigning values to a property of a function will usually cause those members to be implicitly declared on the function | ||
* even if they were were not declared (expando functions) | ||
* DTE needs to detect these members and error on them since this behavior is not supported in isolated declarations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is DTE? What does it stand for?
edit: @jakebailey pointed me to his earlier question. The term DTE needs to be changed to something simpler and more meaningful.
In "DTE needs to detect", is DTE a separate executable? A separate compilation code path invoked by a flag? Or just a separate emit code path?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The term DTE was defined as a ".d.ts
-emitter" by @RyanCavanaugh in this post from March 2023 and has been used in the context of this project semi-frequently since then.
It refers to a tool that can take exactly one .ts
file and emit exactly one .d.ts
file as a simple syntactic transform. It is expected that all future DTEs will match the reference implementation in TypeScript.
export function bindSourceFileForDeclarationEmit(file: SourceFile) { | ||
const nodeLinks: EmitDeclarationNodeLinks[] = []; | ||
function tryGetNodeLinks(node: Node): EmitDeclarationNodeLinks | undefined { | ||
const id = (node as any).id; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
patching id
onto node is probably going to be slow. Could nodeLinks look up some other way, maybe by object identity?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The as any
is left over from when this code was outside TSC. TSC already uses id
in getNodeId
. This just gets the node links if they exist, as opposed to getNodeLinks
which will create them if they don't exist. This is modeled on getNodeLinks
in checker.
* @internal | ||
*/ | ||
export function getMemberKey(name: string | PropertyName | NoSubstitutionTemplateLiteral | undefined): MemberKey | undefined; | ||
export function getMemberKey(name: string | PropertyName | NoSubstitutionTemplateLiteral | undefined): string | undefined { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a very weird type
edit: and a pretty suspicious function -
- when is it passed string, which is unlike the other types?
- when is it passed undefined; would it possible to make sure its inputs are always defined?
- would it be possible to re-use existing functions and add the prefixes on in a smaller function with fewer cases?
getMemberKey, | ||
}; | ||
|
||
function resolveEntityName(location: Node, node: Expression, meaning: SymbolFlags): EmitDeclarationSymbol | undefined { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note this code is actually based on checker, not binder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is true. The resolve*
fuctions would probbalymake more sense to be moved in the resolver but there are resolveName
is used during binding so it's a bit hard to extarct without a circular dependency.
const symbol = createEmitSymbol(); | ||
symbol.declarations.push(node); | ||
setValueDeclaration(symbol as Symbol, node); | ||
node.symbol = symbol as Symbol; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does this assign to node instead of the node's links?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
node.symbol
is accessed in a lot of places in declaration emit. We fill declarations
and valueDeclaration
to allow everything to work. I can try to see if we can remove the need for those, but it will probably mean adding more members to the emit resolver.
} | ||
|
||
function addDeclaration(table: EmitDeclarationSymbolTable, name: MemberKey | undefined, node: Declaration, { includes, excludes }: SymbolRegistrationFlags) { | ||
let symbol = name !== undefined ? getSymbol(table, name) : createEmitSymbol(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
anonymous symbols don't get added to the table? why not?
return addDeclaration(isBlockScoped ? currentLocalSymbolTable : currentFunctionLocalSymbolTable, name, node, flagsAndForbiddenFlags); | ||
} | ||
|
||
function addDeclaration(table: EmitDeclarationSymbolTable, name: MemberKey | undefined, node: Declaration, { includes, excludes }: SymbolRegistrationFlags) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there's a lot of repeated code here and in createAnonymousEmitSymbol/getSymbol. Comparing those with this raises quite a few questions. Probably it would be better to de-dupe the code, making it all behave the same way.
|
||
function addDeclaration(table: EmitDeclarationSymbolTable, name: MemberKey | undefined, node: Declaration, { includes, excludes }: SymbolRegistrationFlags) { | ||
let symbol = name !== undefined ? getSymbol(table, name) : createEmitSymbol(); | ||
// Symbols don't merge, create new one |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The binder issues an errors in this case and merges anyway. How does creating a new symbol affect emit?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it does always merge. From what I understand here, the binder will create a new symbol if symbol.flags & excludes
. I did spend quite a bit of time trying to emulate the same emit behavior, but I'm ultimately not sure how much this matters anyway since these will be error cases and emulating teh same behavior in an error is not crucial
*/ | ||
export function getMemberKeyFromElement(element: NamedDeclaration): MemberKey | undefined { | ||
if (isConstructorDeclaration(element) || isConstructSignatureDeclaration(element)) { | ||
return "@constructor" as MemberKey; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like @
is a name-mangling prefix
I feel like I'm misunderstanding something, then. I thought the diffs were specifically "here's the dts emitted without isolatedDeclarations, here's with isolatedDeclarations, and here's the diff. If there's no diff, then there was no change and we are good to go". |
I have more of a problem with “modules” than “isolated” in If I were starting from scratch, I’d probably make |
I'm sort of struggling to understand the changes to declaration emit (declarations.ts) that occur when |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got through a good bit of the code, but stopped reviewing in order to get myself up to speed on the fairly fundamental question of “why does --isolatedDeclarations
actually affect emit, not just issue diagnostics”
@@ -6227,6 +6227,11 @@ | |||
"category": "Message", | |||
"code": 6718 | |||
}, | |||
"Ensure that each file can have declaration emit generated without type information": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Ensure that each file can have declaration emit generated without type information": { | |
"Ensure that each file can have declaration emit generated without reading other files": { |
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both of these are true. it does allow one file at a time declaration emit, but it also ensures that not types need to be created by the type checker. I feel the latter constraint is the stricter one since it forbids most inference even if the information is available in the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think what’s going on in localInferenceResolver.ts
is “type information,” but I take your point that needing to annotate function return types is probably more visible to users than the lack of cross-file information.
@@ -6741,6 +6746,130 @@ | |||
"category": "Error", | |||
"code": 9006 | |||
}, | |||
"Function must have an explicit return type annotation with --isolatedDeclarations.": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit:
"Function must have an explicit return type annotation with --isolatedDeclarations.": { | |
"Function must have an explicit return type annotation with '--isolatedDeclarations'.": { |
is the typical style. My ear also slightly prefers in '--isolatedDeclarations'
over with
, while the wordier when '--isolatedDeclarations' is enabled
is more established... I don’t have a very strong opinion here.
"category": "Error", | ||
"code": 9012 | ||
}, | ||
"Expression type can't be inferred with --isolatedDeclarations.": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: “cannot” is typical
TranspileDeclarationsOutput, | ||
} from "../../_namespaces/ts"; | ||
|
||
export function transpileDeclaration(sourceFile: SourceFile, transpileOptions: TranspileDeclarationsOptions): TranspileDeclarationsOutput { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: transpileDeclaration
sounds to me like it takes a declaration file and transpiles it to... something else? (I always conceptualized ts.transpileModule
as taking a module, although it also happens to return one.) I don’t immediately have a suggestion for a better name, though 🤔
On that note, what happens when the sourceFile
you provide is a declaration file?
Also, this being the most important API addition, I think it deserves some JSDoc description.
const knownFunctionMembers = new Set([ | ||
"I:apply", | ||
"I:call", | ||
"I:bind", | ||
"I:toString", | ||
"I:prototype", | ||
"I:length", | ||
]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silly idea: I was thinking you could statically validate this list against keyof Function
as it exists during the compiler’s own compilation. However, trying that out shows that this list omits arguments
, caller
, and name
. Is that intentional?
// We forbid references in isolated declarations no need to report any errors on them | ||
if (isolatedDeclarations) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m not quite following this. This function doesn't just get called for literal triple slash references in the input file; it also gets called when you write an import that resolves to an ambient module. And returning early here changes the emit in a potentially dangerous way, without an accompanying isolatedDeclarations
error. For example, if I have @types/node
installed and a source file like
export { readFile } from "fs";
This is legal according to the PR as it stands, but the emit under --isolatedModules
loses the triple-slash reference that makes "fs"
resolvable:
- /// <reference types="node" />
export { readFile } from "fs";
Based on the analog to isolatedModules
, my expectation for how this flag would work was that
- Enabling the flag wouldn’t change emit with
tsc --declaration
- Enabling the flag would leverage whole-program info to issue diagnostics in cases like the above, where information from other files (or too-complex type inference) was necessary to produce the correct emit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, I will change the code to make TS emit equivalent.
Just to understand the implications of not emitting these reference directives in transpileDeclaration
:
Without these ts will not be able to resolve import of modules that are declared in the declarations of other modules. Such as node
having a declare mdoule 'fs' { ... }
. (corect e on the terminology here 😅)
For node this is not actually that big an issue from what I can tell since getAutomaticTypeDirectiveNames
will automatically add all packages in @types
, and node types are defined there.
For types that come from regular imports that resolve to a package in node_modules
that contains the appropriate declarations, this again is not an issue.
The only way the absence of these directives becomes a problem is if the types for a module are not in the usual resolution path, and if the module is not in the typeRoots
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’ve skimmed through most of the remaining changes to declarations.ts, and there are many cases of
if (isolatedDeclarations) {
// return something different
}
Were these all intended to be if (context.kind === TransformationContextKind.Isolated)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, just to be clear, I don’t think that matching emit here is sufficient; I do think isolatedDeclarations
needs to issue an error. I used @types/node
as a simple example, and you’re right that in practice it’s very unlikely for the end user not to already have @types/node
loaded due to other references. But that’s totally circumstantial. We emit those triple slash references for a reason; we can’t just say they’re universally unnecessary and silently elide them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, just to be clear, I don’t think that matching emit here is sufficient; I do think
isolatedDeclarations
needs to issue an error. I used@types/node
as a simple example, and you’re right that in practice it’s very unlikely for the end user not to already have@types/node
loaded due to other references. But that’s totally circumstantial. We emit those triple slash references for a reason; we can’t just say they’re universally unnecessary and silently elide them.
We can definitely error on it, the problem is I don't know what the workaround would be.
We struggled previously with reference directives. TS will not just add them, but will also remove them if they are not needed for the declaration file. This is the kind of analysis we can't do in isolated declaration emit. We discussed and decided with @DanielRosenwasser that banning all reference directives in isolated declaration mode would be the way to go.
But due to this ban, it means that a user would be stuck. You can't export * from 'fs'
without a reference directive, but adding it would be an error because we don't know if we need to keep it in the declaration or not.
I’ve skimmed through most of the remaining changes to declarations.ts, and there are many cases of
if (isolatedDeclarations) { // return something different }Were these all intended to be
if (context.kind === TransformationContextKind.Isolated)
?
I will check them again tomorrow, but the if (isolatedDeclarations)
should be in cases where the if
triggers an error, or if an error would have been triggered already. In such cases declaration emit should not happen anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TS will not just add them, but will also remove them if they are not needed for the declaration file.
I’m not sure I was aware of this. Sorry to jump into a conversation you’ve already had, but did we consider changing normal declaration emit to preserve all manually written reference directives?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@andrewbranch This example shows the removal of the reference directive.
//index.ts
/// <reference types="node" />
export function x() { }
//index.d.ts
export declare function x(): void;
I am not sure about keeping them in either. Wouldn't that keep implementation details in the declaration file? I am fine with always keeping them. I don't think people are adding these anymore for other reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it’s no different from keeping import "./foo"
statements in declaration emit. The user wrote it on purpose and it changes the shape of their program; it feels odd to me that we would elide it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will change it to always preserve reference directives with --isolatedDeclarations
The simple answer is that it does not. Running the command line tsc with The slightly more complicated answer is that if you use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More comments on emitBinder
if (isBindingPattern(d.name)) { | ||
function bindBindingPattern(pattern: BindingPattern) { | ||
// type BindingPattern = ObjectBindingPattern | ArrayBindingPattern; | ||
(pattern.elements as NodeArray<ArrayBindingElement>).forEach(b => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it safe to cast to ArrayBindingElement, ignoring [Object]BindingElement?
(pattern.elements as NodeArray<ArrayBindingElement>).forEach(b => { | |
for (const b of pattern.elements as NodeArray<ArrayBindingElement>) { |
bindNode(d.initializer); | ||
} | ||
if (isBindingPattern(d.name)) { | ||
function bindBindingPattern(pattern: BindingPattern) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these nested functions are not great for performance, since bindVariable will be called a whole lot.
statements.forEach(statement => { | ||
if (!filter(statement)) return; | ||
bindNode(statement); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
statements.forEach(statement => { | |
if (!filter(statement)) return; | |
bindNode(statement); | |
}); | |
for (const statement of statements) { | |
if (filter(statement)) bindNode(statement); | |
}; |
(also this is simple enough I'd be very tempted to inline it.)
bindContainer(nodes, n => n.kind !== SyntaxKind.FunctionDeclaration); | ||
} | ||
function bindExpandoMembers(expression: Node) { | ||
if (isBinaryExpression(expression) && expression.operatorToken.kind === SyntaxKind.EqualsToken) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
invert this and early-return
} | ||
else { | ||
const argumentExpression = assignmentTarget.argumentExpression; | ||
name = factory.createComputedPropertyName(argumentExpression); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of creating a computed property name wrapper for argumentExpression
, make getMemberKey handle directly whatever values it might have
Based on my reading of the code (which could be wrong), I'd recommend extracting the computed property part of getMemberKey
into a separate function and then calling it directly from here, or calling a small wrapper of it. If this is the only place that getMemberKey is called with a computed property name, then I think the code might even be simplified further.
const assignmentTarget = expression.left; | ||
const isPropertyAccess = isPropertyAccessExpression(assignmentTarget); | ||
if ( | ||
(isPropertyAccess || isElementAccessExpression(assignmentTarget)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same--invert and early-return
isPrefixUnaryExpression(computedName) | ||
&& isNumericLiteral(computedName.operand) | ||
) { | ||
if (computedName.operator === SyntaxKind.MinusToken) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does typescript do constant folding like this? I don't remember it but I could easily have missed it being added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is similar to what getPropertyNameForPropertyNameNode
does. I will change over to use __String
and the regular way of escaping names.
} | ||
} | ||
|
||
function canHaveExpandoMembers(declaration: Node) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you mind looking for an existing utility for this? I thought there was one.
if (declaration.type || !isVarConst(declaration)) { | ||
return false; | ||
} | ||
if (!(declaration.initializer && isFunctionExpressionOrArrowFunction(declaration.initializer))) { | ||
return false; | ||
} | ||
return true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (declaration.type || !isVarConst(declaration)) { | |
return false; | |
} | |
if (!(declaration.initializer && isFunctionExpressionOrArrowFunction(declaration.initializer))) { | |
return false; | |
} | |
return true; | |
return !declaration.type | |
&& isVarConst(declaration) | |
&& declaration.initializer | |
&& isFunctionExpressionOrArrowFunction(declaration.initializer); |
though it might work even better to return a single predicate for the whole function starting with
return isFunctionDeclaration(declaration) || ...
src/compiler/checker.ts
Outdated
@@ -48221,13 +47957,25 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |||
hasSyntacticModifier(parameter, ModifierFlags.ParameterPropertyModifier); | |||
} | |||
|
|||
function isExpandoFunctionDeclaration(node: Declaration): boolean { | |||
const declaration = getParseTreeNode(node, isFunctionDeclaration); | |||
function isExpandoFunction(node: Declaration): boolean { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The checker should already handle both functions declarations and function expressions assigned to variables. Either this function was previously incorrect, or its usage is intended as the first of a pair of declaration/assignment checks.
Either way it shouldn't need to start handling variable declarations.
src/compiler/checker.ts
Outdated
return false; | ||
} | ||
|
||
// Only identifiers of the for A.B.C can be used |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Only identifiers of the for A.B.C can be used | |
// Only identifiers of the form A.B.C can be used |
src/compiler/checker.ts
Outdated
return isFreshLiteralType(getTypeOfSymbol(getSymbolOfDeclaration(node))); | ||
} | ||
return false; | ||
} | ||
function isLiteralComputedName(node: ComputedPropertyName) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm less familiar with computed property handling, but this code must also already exist in the checker in order for it to work. So I expected to see some use of it inside the checker instead of just exporting it.
@@ -1384,6 +1384,20 @@ export type HasIllegalModifiers = | |||
| MissingDeclaration | |||
| NamespaceExportDeclaration; | |||
|
|||
/** @internal */ | |||
export type HasInferredType = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bikeshed: HasInferrableType
?
@@ -5666,7 +5680,26 @@ export enum TypeReferenceSerializationKind { | |||
} | |||
|
|||
/** @internal */ | |||
export interface EmitResolver { | |||
export interface CoreEmitResolver { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, I wonder why there are new Core* versions of interfaces. Is it Core=public, original=internal? But the new ones are marked internal too.
NotKeepLiterals = ~KeepLiterals, | ||
} | ||
|
||
const relatedSuggestionByDeclarationKind = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My initial reaction is that the checker should be the one issuing the errors, in order to reduce duplication.
So, a summary from the design meeting: From the outside, we see the feature as having four parts:
Unfortunately, the part we reached consensus on is that the current implementation is too much of a re-implementation. In my opinion, at least, there's too much to change and review before the end of the week, when it would need to be ready for 5.4 beta. We were split on several other issues and ran out of time to decide on them:
My opinionMy specific complaint is: things that are illegal under isolatedDeclarations should be implemented as errors in the checker, using existing inference code from the checker. Code that's in the compiler should re-use the existing binder, checker, etc. Otherwise we're signing up to maintain two Typescript compilers at full correctness, etc. If that really is necessary, the second compiler should be clearly separate from the first one. |
Signed-off-by: Titian Cernicova-Dragomir <[email protected]>
Signed-off-by: Titian Cernicova-Dragomir <[email protected]>
@sandersn I think this PR would address some of the concerns around code duplication: bloomberg#138 |
Signed-off-by: Titian Cernicova-Dragomir <[email protected]>
Remove binder from transpileDeclaration implementation.
As discussed in here we are reevaluating our approach. |
Fixes #47947
Design Goals
When adding this new flag to TypeScript, these are the design goals we had in mind:
Non goals:
Status
The work-in-progress branch is here which implements the flag:
https://github.com/bloomberg/TypeScript/tree/isolated-declarations
The new standalone declaration emitter is located as a sub-package here:
https://github.com/bloomberg/TypeScript/tree/isolated-declarations/external-declarations
There are three deliverables:
restrictions.
Restrictions
When
--isolatedDeclarations
is enabled, TypeScript will enforce stricter rules than usual on the user's source code. These restrictions are necessary in order to keep the Declaration Emit to be a simple one-in-one-out file transform.R1. Do not print synthetic types
This is the main restriction that enables isolated declarations to work. Type annotations will be required on:
The requirement only exists if the symbol makes it into the declaration file (directly by exporting or indirectly by referencing in another exported symbol)
For example these are now errors with isolated declarations:
The workaround is to add explicit type annotations as required.
R2. Default exports
Default exports can’t have a type annotation. If the default export is an expression, TypeScript currently transforms it to a variable with a synthesized type annotation and rewrites the default export to use this new variable:
Isolated declarations can’t synthesize new types so such a construct is effectively forbidden. To work around this, users will have to do manually what declaration emit does automatically, namely create a separate variable declaration and add an explicit type annotation to that variable.
See Possible DX improvements for potential improvements.
R3. Expando functions
What we mean by expando functions are functions that have extra properties attached to them:
While determining the properties to be added to the generated namespace associated with the function is a syntactic process that could be supported in isolated declarations, there is no place to place annotations on the added properties (ex foo). Since there is no place to add the type annotation on these extra members there is no way to preserve this behavior in isolated declaration mode.
The workaround is to use namespace-function merging to declare the properties. This was the recommendation to achieve this before expado-functions were supported.
See Possible DX improvements for potential improvements.
R4. No synthetic extends clauses
For patterns where the extends clause of a class is an expression, current declaration would produce an new variable and add the type of the extends clause to the new variable, and then change the extends
Isolated declarations emit can’t synthetic types for the generated variable, so this pattern can’t really be supported in this mode.
The workaround is to manually create a base class variable in code and use that in the extends clause (possibly using some for of relative typing to not have to duplicate the class type):
R5. No expressions in enums
TypeScript will compute the value of enum members that are assigned a value.
Isolated declarations can’t easily allow this as the expressions can contain cross module expressions:
Under isolated declarations we will get an error in E2 as we are assigning a computed value of
E1.A
See Possible DX improvements for potential improvements.
R6. Destructuring in declarations
Currently declaration emit flattens out variables that result from destructuring:
This approach requires type information that an external tool would not have. A workaround would be to not use destructuring as part of anything that makes it into the declaration file:
See Possible DX improvements for potential improvements.
R7. Import required by augmentation is an error
Currently TypeScript removes imports from declarations if none of their types as used in the declaration. This is generally generally safe, except for the case when the import actually augments the module, such as in this example (taken from
tests\cases\compiler\declarationEmitForModuleImportingModuleAugmentationRetainsImport.ts
)The current version of isolated declarations would make this code an error, as there is no way from
parent.ts
to tell that the import ofchild1
is required for its augmentation ofparent.ts
and thus will be removed by a tool without knowledge of this.Emit Changes
These are notable changes in the generated output from the new standalone Declaration Emitter compared to the pre-existing declaration emit.
E1. Do not remove property aliases from binding elements in function parameters
Typescript will attempt to remove the renaming of binding element in function parameters, but is inconsistent about doing this:
The example above is due to the fact that when deciding whether to preserve the rename or not the declaration emitter uses information about general usage of the variable rather than information about whether it is used in the function signature (which does not exist at present)
In order to ensure consistency, at least for now this rename is always preserved in declaration files even if it is unused. Improving the situation around this is a separate issue that when implemented can be supported in isolated declaration. (An isolated declaration emitter could track the usage of the renamed symbol in the function signature)
E2. Print initializers as written
TypeScript will normalize literal values in declaration files:
To simplify emit, under isolated declarations initializers are printed as written. Potentially we could expand this to regular declaration emit. This seems like a better DX, the initializers usually use a specific syntax for reasons of readability so preserving the original text might be a good idea.
E3. Do not consider external symbol meaning in isolated declarations
This means that if an imported symbol has a meaning (type vs value) that is different from the meaning it is used in the declaration, we might keep unused imports that are preserved in the declaration file:
The test above will generate for app.ts the following declaration under isolated declarations:
But this declaration without isolated declarations:
This is because $ is used as a value, but imported as a type. Using the type checker, the compiler can figure out that the imported $ is a type and thus the import is unused in the resulting declaration. In isolated declaration mode, we don’t actually have this information. Leaving the import behind seems bening as it will essentially be unused.
Note: A suggestion to ensure this is not a problem is to use verbatim imports with isolated declarations
Implementation notes
I1. Transformations of paths in path reference directives
In order to generate the same declarations as TypeScript, we have to perform some manipulation of paths in
/// <reference path="..." />
directives:Remove paths that include
node_modules
Test that express this behavior:
umd-augmentation-2
,`umd-augmentation-4``This behavior is hardcoded in declaration emit:
Remove references to files that are not part of the current project
This requires knowing beforehand what files are part of the project. This does not violate the design goal of not depending on information in other files as it does not require looking into the file itself, only knowing what other files exist.
When trying to find files, if the referenced file does not have an extension we will try to find a file based on a list of known extension (
.ts, .d.ts, .cts, .d.cts, .d.mts, .d.mts
).Test that show this behavior:
duplicateIdentifierRelatedSpans6, duplicateIdentifierRelatedSpans7, fileReferencesWithNoExtensions, moduleAugmentationDuringSyntheticDefaultCheck
If the file is not found the
/// <reference path="..." />
directive should be removed.Test that show this behavior
declarationEmitInvalidReference2, declarationEmitInvalidReferenceAllowJs, invalidTripleSlashReference, selfReferencingFile2, parserRealSource1, parserRealSource10, parserRealSource11, parserRealSource12, parserRealSource13, parserRealSource14, parserRealSource2, parserRealSource3, parserRealSource4, parserRealSource5, parserRealSource6, parserRealSource7, parserRealSource8, parserRealSource9, parserharness, parserindenter, scannertest1, consumer, matchFiles
Change extension to appropriate declaration extension
After finding the file in the current project, if the extension is not a declaration file extension (a file that ends with
.d.ts, .d.mts, .d.cts or .d.ext.ts, .d.ext.mts, .d.ext.cts
- the latter 3 are for--allowArbitraryExtensions
flag) change the file extension to a declaration file extension appropriate for the original file type ( .mjs and .mts become .d.mts, .cjs and .cts become .d.cts, other extension become .d.ts)Example:
/// <reference path="aliasOnMergedModuleInterface_0.ts" />
turns to
/// <reference path="aliasOnMergedModuleInterface_0.d.ts" />
Test that show this behavior
aliasOnMergedModuleInterface, aliasUsedAsNameValue, ambientExternalModuleWithInternalImportDeclaration, ambientExternalModuleWithoutInternalImportDeclaration, arrayOfExportedClass, commentOnAmbientClass1, commentOnAmbientEnum, commentOnAmbientModule, commentOnAmbientVariable2, commentOnAmbientfunction, commentOnElidedModule1, commentOnInterface1, commentOnSignature1, constDeclarations-access5, crashInResolveInterface, declFileAmbientExternalModuleWithSingleExportedModule, declFileForExportedImport, doNotEmitTripleSlashCommentsInEmptyFile, doNotEmitTripleSlashCommentsOnNotEmittedNode, doNotemitTripleSlashComments, duplicateIdentifierRelatedSpans6, duplicateIdentifierRelatedSpans7, emitMemberAccessExpression, emitTopOfFileTripleSlashCommentOnNotEmittedNodeIfRemoveCommentsIsFalse, enumFromExternalModule, exportAssignClassAndModule, exportAssignedTypeAsTypeAnnotation, exportAssignmentOfDeclaredExternalModule, exportAssignmentOfGenericType1, exportEqualCallable, exportEqualErrorType, exportEqualMemberMissing, externalModuleAssignToVar, externalModuleRefernceResolutionOrderInImportDeclaration, fileReferencesWithNoExtensions, genericWithCallSignatures1, importAliasFromNamespace, importDecl, importDeclarationUsedAsTypeQuery, importUsedInExtendsList1, import_unneeded-require-when-referenecing-aliased-type-throug-array, instanceOfInExternalModules, jsFileCompilationErrorOnDeclarationsWithJsFileReferenceWithNoOut, jsFileCompilationErrorOnDeclarationsWithJsFileReferenceWithOut, jsFileCompilationErrorOnDeclarationsWithJsFileReferenceWithOutDir, jsFileCompilationNoErrorWithoutDeclarationsWithJsFileReferenceWithNoOut, jsFileCompilationNoErrorWithoutDeclarationsWithJsFileReferenceWithOut, localAliasExportAssignment, memberAccessMustUseModuleInstances, mergedInterfaceFromMultipleFiles1, missingImportAfterModuleImport, moduleAliasAsFunctionArgument, moduleAugmentationDuringSyntheticDefaultCheck, moduleInTypePosition1, moduleSymbolMerging, nodeResolution4, outModuleTripleSlashRefs, privacyCannotNameAccessorDeclFile, privacyCannotNameVarTypeDeclFile, privacyFunctionCannotNameParameterTypeDeclFile, privacyFunctionCannotNameReturnTypeDeclFile, privacyTopLevelAmbientExternalModuleImportWithExport, privacyTopLevelAmbientExternalModuleImportWithoutExport, propertyIdentityWithPrivacyMismatch, protoAsIndexInIndexExpression, requireEmitSemicolon, reuseInnerModuleMember, selfReferencingFile, selfReferencingFile3, sourceMapWithMultipleFilesWithCopyright, staticInstanceResolution3, tripleSlashReferenceAbsoluteWindowsPath, typeofAmbientExternalModules, underscoreTest1, voidAsNonAmbiguousReturnType, withImportDecl, ambientDeclarationsExternal, topLevelAmbientModule, topLevelModuleDeclarationAndFile
Ensure path is relative to directory of current file
All paths should be normalized (ie use / as a directory separator) and they should be expressed as paths relative to the current directory (no ./ required before files in the current directory, absolute paths should be converted)
Examples:
Test that show this behavior
ambientExportDefaultErrors, conditionalTypeVarianceBigArrayConstraintsPerformance, duplicateIdentifierRelatedSpans6, duplicateIdentifierRelatedSpans7, errorInfoForRelatedIndexTypesNoConstraintElaboration, fileReferencesWithNoExtensions, importAliasFromNamespace, maxNodeModuleJsDepthDefaultsToZero, moduleAugmentationDuringSyntheticDefaultCheck, moduleNodeImportRequireEmit, moduleNodeImportRequireEmit, moduleNodeImportRequireEmit, moduleNodeImportRequireEmit, outModuleTripleSlashRefs, selfReferencingFile3, styledComponentsInstantiaionLimitNotReached, tripleSlashReferenceAbsoluteWindowsPath, typeReferenceDirectives4, typeReferenceDirectives6, untypedModuleImport_vsAmbient
DX Improvement Opportunities
This is a non-exhaustive list of further improvements we could make to the current implementation to require fewer type annotations. These enhancements would ease the adoption of the feature by existing codebases and reduce the verbosity of the source code.
D1. Allow Object and Array literals in const assertions in declarations
Currently the following is an error under isolated declarations:
While allowing any arbitrary expression in the initializer would be confusing, we can extend the current ability to preserve primitive literals in initializers to also preserve object and array literals if they are subject to a const assertion. So the above code should generate the following declaration:
Also allow function signatures in the initializer
We can also consider expanding the above syntax to allow method signatures in object literals. This would be a minimal transform. This is however an addition to syntax that looks like JS syntax, although I would argue once you put the declare word in front of the variable, we are in the TS grammar scope not the JS one.
With this proposal the following object literal will be present in declarations:
D2. Allow inference from expression if there is no need to use non-local type info
This is an alternative to D1. Instead of expanding what we allow in initializers we could allow inference from expressions if the inference only uses type information in the current expression. This restriction should simplify the work a declaration emitter would need to do to a small subset of TypeScript inference that can safely be re-implemented across several tools
This would mean that expressions that use object literals, array literals, and primitive literals can have their types inferred. As well as allowing inference of any methods/ functions in the object without requiring new syntax (as D1 does).
Examples of code that would be inferred:
Examples of code that would not be inferred:
Use
typeof
operatorWe could use the
typeof
type operator to infer types for assignments from other symbols accessible in the file:The above code uses
const
andreadonly
fields to demonstrate this behavior. This is becauselet
and mutable fields will widen the type of the assigned variable, so usingtypeof
without some sort of intrinsicWiden
type would not replicate TS behavior:For the example above TypeScript infers for
v1
the typenumber
because the type is widened. If we add an intrinsicWiden
type we could use thetypeof
type operator to typev1
relative tov0
usingWiden<typeof v1>
. TheWiden
type would replicate any type widening behavior TypeScript has when assigning a value of the specified type to a mutable variable.The suggestion to use
typeof
has the unwanted consequence that the declarations might expose more implementation details than it currently does, but the DX improvements might be worth it.D1 vs D2
The two improvement proposals are competing to solve the same issues and only one should be selected.
D1 allows declaration emitters to remain simpler, at the cost of DX (more type annotations are needed) and more changes to current declaration emit are needed to support some scenarios.
D2 allows more scenarios and requires less changes to current declaration emit, at the cost of putting more complexity in declaration emitters. There is also the risk of TypeScript changing rules with regard to variable inference. This happens infrequently but when it does it will require declarations emitter to add support for any such changes.
D3. Allow Basic Arithmetic Operators in Enum Expressions
As described above any non primitive literal expression is an error under isolated declarations. To alleviate this problem we can allow expressions that contain arithmetic operators, with primitive and dotted identifier operands.
So for example these would be legal with this proposal:
D4. Extract function type from function expression assignment.
Currently this would be an error under isolated declarations:
This is because the fn variable does not have an explicit type annotation.
Synthesizing a type from the assignment is however trivial, as all the information is already present in the arrow function signature (both parameter and return type are specified)
This could apply equally to arrow functions and function expressions.
This could also apply to class fields that are initialized with a function.
D5. Destructured elements - Do not flatten binding elements
As detailed above the following code would currently be an error:
An improvement would be to keep the binding element ‘as is’ in the declaration file:
Allow initialisers
A problem might occur if we also have initializers as the type of the initializer can contribute to the type of the variable:
Similarly with the way we allow const declarations to keep their initializers we can allow initializers to be preserved in destructuring for const declarations. The above code would create the following declaration:
D6. Default Exports
Currently there is no way to be explicit about the type of a default export expression. Adding the ability to specify a type annotation to a default export would help the DX in isolated declarations. The proposal to add this syntax is tracked here,
D7. Extract type from type assertions
Currently type assertions are not considered during declaration emit, so the following code would be an error under isolated declarations:
We could take the type in the assertion and use it as the type of the variable.
This approach could also be used for expando functions:
The problem with this approach is that it encourages over use of type assertions, which are not safe. We could encourage the use of expr satisfies T as T which seems to provide as close to a type annotation as possible by both forcing the expression to have a specific type and checking that the expression satisfies the given type. Ex:
A similar approach could be use for default exports even in the absence of explicit syntax to annotate the default export:
D8. Mark internal APIs
We could also allow users to opt into ignoring isolated declaration errors on a case by case basis, either using
@internal
or@ts-expect-error
/@ts-ignore
. This would allow users to mark APIs that aren’t actually exposed to the outside world (because they are not reachable reachable through public end points)When generating the declaration unknown can be used as the type for any missing annotation
Open issues
These open questions require a decision to be made before the solution can be considered complete.
O1. Computed property names
TypeScript currently uses the type of the expression in a computed property name to tell if two members belong to the same symbol. This does rely on type information being present and so can’t be duplicated in isolatedDeclarations mode. This problem is that for method overloads it is difficult to identify what constitutes a method group.
Example of the problem
Currently the external isolated declaration relies on using the same identifier (or dotted identifier chain) for the grouping of methods.
A proposed solution is that we could add a restriction that overloaded methods must use the same identifier in the computed property name expression.
O2. No transformation of type reference directives
O2.a. Adding/removing directives
TypeScript adds/removes type reference directives. This happens based on the usage of symbols in the declaration file. This is an analysis an external declaration emitter can’t do. It requires loading all library files in order to have access to the symbols, which is antithetical to the “allow per file declaration emit” design goal.
Example of references being removed:
typeReferenceDirectives10
Declaration emit for app.ts
Some other tests that are impacted by this behavior
declarationFilesWithTypeReferences4, moduleResolutionWithSymlinks_preserveSymlinks, moduleResolutionWithSymlinks_referenceTypes, tripleSlashTypesReferenceWithMissingExports, typeReferenceDirectives10, typeReferenceDirectives13, typeReferenceDirectives3, typeReferenceDirectives4, typeReferenceDirectives5, typeReferenceDirectives7, typeReferenceRelatedFiles, nodeModulesTripleSlashReferenceModeDeclarationEmit6, nodeModulesTripleSlashReferenceModeDeclarationEmit7, nodeModulesTripleSlashReferenceModeOverride1, nodeModulesTripleSlashReferenceModeOverride2, nodeModulesTripleSlashReferenceModeOverride3, nodeModulesTripleSlashReferenceModeOverride4, nodeModulesTripleSlashReferenceModeOverride5, nodeModulesTripleSlashReferenceModeOverrideModeError, nodeModulesTripleSlashReferenceModeOverrideOldResolutionError, library-reference-*, library-reference-scoped-packages, typingsLookup1, typingsLookup3
Example of references being added:
typeReferenceDirectives11
Declaration emit for
mod1.ts
Some other tests that are impacted by this behavior
declarationEmitHasTypesRefOnNamespaceUse, declarationFilesWithTypeReferences2, typeReferenceDirectives11, typeReferenceDirectives2, typeReferenceDirectives8
Conclusion: Adding the reference depends on knowing about global types defined in other files, while removing them is based on those library types not being used in the file. Both of these require information from other files which we can’t access in isolated declaration mode.
Options: Here are some options we have
Option 1 would make it difficult for clients to know what libs are required by the types. Option 2 would be more compatible, but can potentially require declarations that are only needed for type checking but are not need for declarations, there is simply not enough information to perform this analysis in isolated declaration.
Note: After discussing with Daniel Rosenwasser, Option 1 seems preferable, but we need to explore what impact this has on projects that consume such declarations.
O2.b.
resolution-mode
in directivesSome type directives have their resolution mode changed during printing (either changed or removed). This is not a problem in itself but if we include type directives, this is something that is not obvious from the reference declaration emitter.
TS printer code:
Some other tests that are impacted by this behavior
nodeModulesTripleSlashReferenceModeDeclarationEmit1, nodeModulesTripleSlashReferenceModeDeclarationEmit2, nodeModulesTripleSlashReferenceModeDeclarationEmit5