-
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
[ID-Prep] PR4 - Preserve types nodes from type assertions in declaration emit #57589
[ID-Prep] PR4 - Preserve types nodes from type assertions in declaration emit #57589
Conversation
export const c = () => null as any as {[K in manyprops]: {[K2 in manyprops]: `${K}.${K2}`}}; |
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.
Changed this to preserve the original intent of the test (test what happens when huge type nodes need to be printed).
f996402
to
9399b32
Compare
tests/baselines/reference/accessorDeclarationEmitVisibilityErrors.errors.txt
Outdated
Show resolved
Hide resolved
@@ -282,6 +296,7 @@ export function transformDeclarations(context: TransformationContext) { | |||
const resolver = context.getEmitResolver(); | |||
const options = context.getCompilerOptions(); | |||
const { noResolve, stripInternal } = options; | |||
const strictNullChecks = getStrictOptionValue(options, "strictNullChecks"); |
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.
Feels weird to have this detail in the transform; right now the only options that are used are ones explicitly related to declaration emit (noResolve/stripInternal).
My impression is that isolatedDeclarations is not assumed to know the compiler options; is the case that we can treat strictNullChecks as true for emit with the assumption that undefined
/null
are just going to be ignored by non-strict users?
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 is a bit of strictNullChecks
that needs to be taken into account. This is used here in the context of inferring optional property declaration types. If strictNullChecks
is on we need to add undefined
to the type otherwise we don't. Without taking this into account any local inference optional declarations will not be possible:
class X {
// x?: 1 | 2 | undefined; with !strictNullChecks
// x?: 1 | 2; // with !strictNullChecks
x? = 1 as 1 | 2
}
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.
Sure but if a user doesn't have strictNullChecks enabled, aren't the two equivalent? Since the way that strictNullChecks=false works is to effectively treat null/undefined as though they don't exist.
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.
They are usually, but if you take the declaration from a project with !strictNullChecks
and use them in a project exactOptionalPropertyTypes
then the missing | undefined
will make a difference.
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.
Thinking about it, it might actually be an improvement. Because a !strictNullChecks
project should be fine with a undefined
.
I can change this to always add | undefined
but the second problem is that it will make emit different between the case when we take the type from the checker (which does not add undefined) and when we try to get the type from the assertion. The solution would be to add the |undefined
always form the checker as well.
Maybe this should be a different PR though? One that always adds |undefined
to optional properties even if !strictNullChecks
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 guess I see what you mean, in that this PR is only trying to touch assertions and therefore making this code unconditional would not change any other 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.
Exactly. Even it's final form, !isolatedDeclarations
some types will still come from the checker, I don't think adding undefined just sometimes would be a good thing.
c47b97d
to
4f63418
Compare
…eclrations-from-assertions
4ffc67a
to
5506934
Compare
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 still don't feel too great about declaration emit directly knowing about strictNullChecks as I'm not sure how we are expecting third party emitters to also know this, but, I guess it's fine.
That's a good point. Aside from |
I think the reason I think it's "fine" is that dependence is already present today, just that it's hidden by calls to the checker. So as a whole, this PR is just an emit improvement and not actually regressing in that way. Third party tools are already able to resolve tsconigs, I just think it's frustrating that declaration emit is sensitive to it at all (but I'm also in the camp of wanting to ditch strictNullChecks=false altogether!) |
My biggest issue is that it will be added sometimes. When the type is extracted in declarations, we will add type P = number | undefined
declare function getP(): P
class X {
f1? = getP() as P; // P | undefined with this PR
f2? = getP(); // P
} This seems like a strange difference that will make people open issues. I think our options are:
I am not 100% we can keep |
We'll discuss this PR in the design meeting on Friday and assuming there are no other concerns around |
@typescript-bot pack this |
Starting jobs; this comment will be updated as builds start and complete.
|
Hey @rbuckton, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build and an npm module you can use via |
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.
IMO, this is something we could/should have done even without isolated declarations, and we've done things like this in the past. We already have machinery for cloning/reusing input type nodes in new locations inside the emit resolver (usually for signatures copied from one location to another by structural inlining) - this logic should be moved into it, too, so all the guardrails around node copying are in one place (and there are a fair few). Over in createTypeOfDeclaration
and createReturnTypeOfSignatureDeclaration
they both defer to nodeBuilder.typeToTypeNode
- within the node builder, we have a serializeExistingTypeNode
(and parent function serializeTypeForDeclaration
used extensively by the JS declaration emitter) function that adds a lot of guard rails onto this node copying process, because, well, there's a lot that needs to be checked for that currently isn't in this PR.
IMO, this logic should be exposed on the nodeBuilder
and then used by the EmitResolver
's createTypeOfDeclaration
and createReturnTypeOfSignatureDeclaration
instead of typeToTypeNode
directly (rather than built directly into the declarations
transformer). That way it's easy to leverage the existing caching and safe copying mechanisms we have, and to reuse the logic in the JS declaration emitter (which, I dunno if you wanna consider testing that out of scope or not).
The goal of the PRs to align emit is to extract more information directly from the initialisers of declarations that have them. To this end we will extract extract types from:
The general idea is that the types in these cases are trivial enough that by looking at the initialization expressions it is possible to derive the type node, without having to go through the type checker. This approach may yield a more readable type in some cases but not necessarily. The end goal is to allow external tools to perform the same transformations, and to be able to guarantee that these transformations are indeed just syntactical ones that don’t use any type information that is not local to the initialiser. Option 1: Transform initialisers to type nodes in the type checker (node builder)This approach will have all the logic for extracting the types in the same place as type nodes are synthesized from type checker types. Copying of existing type nodes already happens in Advantages
DisadvantagesThe declaration transform will not be runnable without the type checker. This creates several problems:
Option 2: Transform initialisers in the declaration emitThis is the approach in the current PR. This approach will have all the logic for extracting types from initialisers in the declaration transform. The declaration transform already does other purely syntactic transformation of original source to ensure the correct types in the declaration (ex: constructor class fields are transformed to parameters and fields, expressions in heritage clauses are moved to variables that are independently typed, default exports are moved to variables) Advantages
Disadvantages
Final thoughtsWhile for the current PR the extraction of the type happens from an assertion, for the other kinds of type from initializer transformations, there is no actual type node to extract from. But rather we go through the literal and create a new appropriate type if possible (reusing as much of the original AST). Putting this second way of getting the type in the typechecker does not really seem like a win. While in some cases the resulting types will be more faithful to the original source, the extra type information can also result in better types from the type checker. Just having the errors without actually extracting the type is also an option, but that makes the validation of external emitters much harder, as TypeScript may emit different declaration files (even if they are mostly semantically equivalent). Extraction from the initializer is needed for isolated declarations, but if it is always better for types to come from this source is not necessarily clear, and it’s a question that is out of scope for the current effort. Option 3: Factor out the shared logicIf we still want to reuse some of this logic in the type checker, maybe a better approach would be to factor out the type node from initializer logic into a separate component that can be reused in both |
Closed in favor of #57772 |
Fixes #57587