Skip to content
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

Remove missing type from tuple type arguments under exactOptionalPropertyTypes #54718

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

Andarist
Copy link
Contributor

fixes #54302

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Jun 20, 2023
@sandersn sandersn requested review from weswigham and jakebailey June 30, 2023 15:48
@jakebailey
Copy link
Member

@typescript-bot test this
@typescript-bot test top100
@typescript-bot user test this
@typescript-bot run dt
@typescript-bot perf test this faster
@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jun 30, 2023

Heya @jakebailey, I've started to run the parallelized Definitely Typed test suite on this PR at 1f43c8d. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jun 30, 2023

Heya @jakebailey, I've started to run the tarball bundle task on this PR at 1f43c8d. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jun 30, 2023

Heya @jakebailey, I've started to run the abridged perf test suite on this PR at 1f43c8d. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jun 30, 2023

Heya @jakebailey, I've started to run the diff-based user code test suite on this PR at 1f43c8d. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jun 30, 2023

Heya @jakebailey, I've started to run the diff-based top-repos suite on this PR at 1f43c8d. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jun 30, 2023

Heya @jakebailey, I've started to run the extended test suite on this PR at 1f43c8d. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jun 30, 2023

Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/155762/artifacts?artifactName=tgz&fileId=0F1688874932DAB1C886AE27B93E66A560200FFA41E8495C6C25360F089A781A02&fileName=/typescript-5.2.0-insiders.20230630.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@5.2.0-pr-54718-7".;

@typescript-bot
Copy link
Collaborator

@jakebailey
The results of the perf run you requested are in!

Here they are:

Comparison Report - main..54718

Metric main 54718 Delta Best Worst p-value
Angular - node (v16.17.1, x64)
Memory used 366,137k (± 0.00%) 366,157k (± 0.01%) ~ 366,129k 366,197k p=0.128 n=6
Parse Time 3.58s (± 0.21%) 3.60s (± 0.32%) +0.02s (+ 0.47%) 3.59s 3.62s p=0.015 n=6
Bind Time 1.18s (± 0.64%) 1.18s (± 0.00%) ~ 1.18s 1.18s p=0.598 n=6
Check Time 9.67s (± 0.57%) 9.67s (± 0.37%) ~ 9.61s 9.71s p=0.520 n=6
Emit Time 8.00s (± 0.48%) 7.99s (± 0.61%) ~ 7.92s 8.05s p=0.747 n=6
Total Time 22.43s (± 0.27%) 22.44s (± 0.29%) ~ 22.36s 22.52s p=0.872 n=6
Compiler-Unions - node (v16.17.1, x64)
Memory used 192,891k (± 0.03%) 193,373k (± 0.69%) ~ 192,728k 196,091k p=0.575 n=6
Parse Time 1.58s (± 1.63%) 1.59s (± 0.76%) ~ 1.58s 1.61s p=0.622 n=6
Bind Time 0.82s (± 0.77%) 0.82s (± 1.09%) ~ 0.81s 0.83s p=1.000 n=6
Check Time 10.15s (± 0.41%) 10.11s (± 0.65%) ~ 10.01s 10.18s p=0.332 n=6
Emit Time 3.02s (± 0.86%) 3.04s (± 1.33%) ~ 2.98s 3.09s p=0.416 n=6
Total Time 15.57s (± 0.34%) 15.56s (± 0.75%) ~ 15.44s 15.71s p=1.000 n=6
Monaco - node (v16.17.1, x64)
Memory used 346,115k (± 0.01%) 346,125k (± 0.01%) ~ 346,088k 346,148k p=0.471 n=6
Parse Time 2.73s (± 0.79%) 2.74s (± 0.67%) ~ 2.72s 2.76s p=0.681 n=6
Bind Time 1.08s (± 0.77%) 1.08s (± 1.08%) ~ 1.07s 1.10s p=0.555 n=6
Check Time 8.01s (± 0.58%) 7.98s (± 0.38%) ~ 7.93s 8.02s p=0.227 n=6
Emit Time 4.51s (± 1.39%) 4.48s (± 0.64%) ~ 4.45s 4.53s p=0.226 n=6
Total Time 16.34s (± 0.71%) 16.28s (± 0.33%) ~ 16.24s 16.38s p=0.422 n=6
TFS - node (v16.17.1, x64)
Memory used 300,218k (± 0.00%) 300,206k (± 0.01%) ~ 300,175k 300,234k p=0.470 n=6
Parse Time 2.16s (± 0.56%) 2.17s (± 0.75%) ~ 2.15s 2.19s p=0.288 n=6
Bind Time 1.23s (± 0.85%) 1.24s (± 0.98%) ~ 1.22s 1.25s p=0.868 n=6
Check Time 7.32s (± 0.24%) 7.33s (± 0.45%) ~ 7.29s 7.38s p=0.332 n=6
Emit Time 4.35s (± 0.66%) 4.31s (± 0.46%) ~ 4.29s 4.33s p=0.062 n=6
Total Time 15.07s (± 0.33%) 15.06s (± 0.39%) ~ 14.99s 15.14s p=0.809 n=6
material-ui - node (v16.17.1, x64)
Memory used 481,659k (± 0.01%) 481,681k (± 0.02%) ~ 481,612k 481,830k p=0.810 n=6
Parse Time 3.26s (± 0.36%) 3.26s (± 0.60%) ~ 3.24s 3.30s p=0.868 n=6
Bind Time 0.95s (± 0.57%) 0.95s (± 0.43%) ~ 0.94s 0.95s p=0.054 n=6
Check Time 18.39s (± 0.61%) 18.45s (± 0.87%) ~ 18.19s 18.61s p=0.423 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 22.61s (± 0.50%) 22.66s (± 0.75%) ~ 22.40s 22.82s p=0.471 n=6
xstate - node (v16.17.1, x64)
Memory used 561,157k (± 0.01%) 561,162k (± 0.02%) ~ 561,060k 561,362k p=1.000 n=6
Parse Time 4.01s (± 0.32%) 4.01s (± 0.33%) ~ 3.99s 4.03s p=1.000 n=6
Bind Time 1.74s (± 1.03%) 1.73s (± 0.47%) ~ 1.72s 1.74s p=0.208 n=6
Check Time 3.06s (± 0.45%) 3.05s (± 0.53%) ~ 3.04s 3.08s p=0.279 n=6
Emit Time 0.09s (± 5.53%) 0.09s (± 5.53%) ~ 0.09s 0.10s p=1.000 n=6
Total Time 8.91s (± 0.31%) 8.88s (± 0.29%) ~ 8.85s 8.91s p=0.123 n=6
System
Machine Namets-ci-ubuntu
Platformlinux 5.4.0-148-generic
Architecturex64
Available Memory16 GB
Available Memory15 GB
CPUs4 × Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
Hosts
  • node (v16.17.1, x64)
Scenarios
  • Angular - node (v16.17.1, x64)
  • Compiler-Unions - node (v16.17.1, x64)
  • Monaco - node (v16.17.1, x64)
  • TFS - node (v16.17.1, x64)
  • material-ui - node (v16.17.1, x64)
  • xstate - node (v16.17.1, x64)
Benchmark Name Iterations
Current 54718 6
Baseline main 6

Developer Information:

Download Benchmark

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the user test suite comparing main and refs/pull/54718/merge:

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Unknown failure"
  • 1 instance of "Package install failed"

Otherwise...

Something interesting changed - please have a look.

Details

rxjs-src

/mnt/ts_downloads/rxjs-src/build.sh

  • [NEW] error TS2428: All declarations of 'WeakMap' must have identical type parameters.
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-54718/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
  • [MISSING] error TS2428: All declarations of 'WeakMap' must have identical type parameters.
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.collection.d.ts(63,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.iterable.d.ts(162,11)
    • /home/vsts/work/1/s/typescript-main/lib/lib.es2015.symbol.wellknown.d.ts(140,11)

@typescript-bot
Copy link
Collaborator

@jakebailey Here are the results of running the top-repos suite comparing main and refs/pull/54718/merge:

Everything looks good!

@typescript-bot
Copy link
Collaborator

Hey @jakebailey, the results of running the DT tests are ready.
Everything looks the same!
You can check the log here.

@@ -15217,7 +15217,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const typeArguments = !node ? emptyArray :
node.kind === SyntaxKind.TypeReference ? concatenate(type.target.outerTypeParameters, getEffectiveTypeArguments(node, type.target.localTypeParameters!)) :
node.kind === SyntaxKind.ArrayType ? [getTypeFromTypeNode(node.elementType)] :
map(node.elements, getTypeFromTypeNode);
map(node.elements, element => removeMissingType(getTypeFromTypeNode(element), element.kind === SyntaxKind.OptionalType));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems strange to me that this is being done inside of getTypeArguments; previously this function effectively just pulled info from the node and that's it, no processing.

Is there no other place this makes sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would go even further and say that the optionality should always be removed from those type arguments (of optional elements).

When it comes to object properties the optionality is added to the declared type based on the optionality marker on the property, see here. I see a strong parallel between this and the element types. The type argument is like a property's declared type - the optionality is a separate piece of information.

In a somewhat similar manner, in the case of a rest element - the type argument is not an array type! It's an element type of that array and the final array type~ gets created when the type argument meets the element flag.

I think removing this from the type arguments is best because the iterated type of an array comes from the Symbol.iterator method. Its signature is defined as (): IterableIterator<T> and T gets instantiated to the union that contains all type arguments. So at the end, we get something like (): IterableIterator<string | number | undefined>.

We might notice here that the rest type fits into this perfectly because its type is not an array type - so if we just push that to the union we end up with its element type and not an array, its concatenable~ with required elements, and the Symbol.iterator method that gets instantiated this way is correct.

It doesn't work well with optional elements in combination with exactOptionalPropertyTypes though if the undefined is included in the type argument right from the start. After all, at runtime - we won't actually iterate over that undefined since undefined isn't assignable to an optional element under EOPT.

So, I run an experiment to remove this at all times and "add back" that optionality at other places (see the commit here). It turned out to be... semi-successful. All the tests pass except the one that I'm trying to fix here.

The problem with this is that the easiest way to add back the optionality is through the created mapper (here). But then the T -> UnionOfTypeArguments uses that as well when instantiating the Symbol.iterator method. I don't see a clean way to branch this somehow or to remove the missing type afterward. After all, it's not only about what we get internally. The Symbol.iterator's method should get instantiated to, let's say, (): IterableIterator<string | number> and not to (): IterableIterator<string | number | undefined>. That's publicly~ visible information that can be obtained through types in user programs.

So, for better or worse - I think that it's best to leave things as-is but avoid adding the missingType to optional type arguments under EOPT. Perhaps a cleaner way of doing this would be something along those lines:

diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 512e191cce..874318fb3b 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -15563,7 +15563,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
             const typeArguments = !node ? emptyArray :
                 node.kind === SyntaxKind.TypeReference ? concatenate(type.target.outerTypeParameters, getEffectiveTypeArguments(node, type.target.localTypeParameters!)) :
                 node.kind === SyntaxKind.ArrayType ? [getTypeFromTypeNode(node.elementType)] :
-                map(node.elements, element => removeMissingType(getTypeFromTypeNode(element), element.kind === SyntaxKind.OptionalType));
+                map(node.elements, element => getTypeFromTypeNode(element.kind === SyntaxKind.OptionalType && exactOptionalPropertyTypes ? (element as OptionalTypeNode).type : element));
             if (popTypeResolution()) {
                 type.resolvedTypeArguments = type.mapper ? instantiateTypes(typeArguments, type.mapper) : typeArguments;
             }

I think I like this one better and I am going to push it out in a second.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also a non-zero chance that this actually makes some of the existing removeMissingType calls redundant. I will investigate this.

@Andarist Andarist requested a review from jakebailey September 3, 2023 08:09
@Andarist
Copy link
Contributor Author

Andarist commented Sep 3, 2023

I managed to clean some things further. Personally, I like the changes - some bits feel simpler to me now. Let me know what do you think.

Comment on lines +12865 to +12867
const propertiesMapper = isTupleType(type)
? createTypeMapper(typeParameters, sameMap(typeArguments, (t, i) => addOptionality(t, /*isProperty*/ true, !!(type.target.elementFlags[i] & ElementFlags.Optional))))
: mapper;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make the overall change smaller in scope we could "flip" the situation here. The existing mapper could be used as propertiesMapper (without the current changes in this PR it would have the missingType in the type arguments) and we could removeMissingType from the "core" mapper here. This way T in Tuple<T> wouldn't contain the missingType but properties would still be instantiated with it.

Alternatively, it's also very likely that we could add the missingType to the tuple.target elements at optional elements. This way we could just have a single mapper here and the missingType would be added to tuple properties at the instantiation time of the .target's fixed elements. Currently, target is represented as smth like [?, ?, ?] - where ? are type parameters. It could be represented as [?, ?, ? | missingType] though. I'm still battling myself how to think about it - would it be a cool trick or an obfuscated solution? 😅 The trick is that the instantiated tuple still wouldn't have missingType in its type arguments but the properties would be automatically read from the instantiated .target - so there would be a tricky mismatch between the tuple's type arguments and its .target. I'm not even sure if that's even possible - I didn't actually try to do it this way, I distinctly recall having such an idea 2 days ago when analyzing this.

That said, I still very much like that the missingType gets removed from the type arguments of the tuple. It feels right to me. The optionality bit is added by the element flag - and it's not part of the "core" type at that position. This way, It would feel a lot closer to me to how it works with object properties where the optionality gets added here. It's part of the computed property type - but not part of the declaredType there.

@@ -16591,7 +16593,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function getTypeFromOptionalTypeNode(node: OptionalTypeNode): Type {
return addOptionality(getTypeFromTypeNode(node.type), /*isProperty*/ true);
const type = getTypeFromTypeNode(node.type);
return exactOptionalPropertyTypes ? type : addOptionality(type, /*isProperty*/ true);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda feel that this could be taken even further. As I mentioned in the other comment - it feels to me that the type argument's type shouldn't include the optionality, even outside of EOPT. The optionality could always be added based on the combination of the type argument and the respective element flag.

I'd rather prefer to explore this as a follow-up change in another PR - if there would be an intereset for it.

@@ -135,15 +138,24 @@ strictOptionalProperties1.ts(211,1): error TS2322: Type 'string | boolean | unde
t = [42, 'abc'];
t = [42, 'abc', true];
t = [42, ,];
~
!!! error TS2322: Type '[number, undefined]' is not assignable to type '[number, string?, boolean?]'.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an extra fix that just happened to be fixed by this change! :)

t = [42, , , ,]; // Error
~
!!! error TS2322: Type '[number, never?, never?, never?]' is not assignable to type '[number, string?, boolean?]'.
!!! error TS2322: Target allows only 3 element(s) but source may have more.
!!! error TS2322: Type '[number, undefined, undefined, undefined]' is not assignable to type '[number, string?, boolean?]'.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently this is how it works:

const arr = [10, ,] as const; // const arr: readonly [10, never?]

I think that this is an improvement though. For the end user, it feels much more readable with undefined here for the omitted expression than never - especially within the error message. I could likely bring back never here though - it's assignable to everything after all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that never only appears here with EOPT and not without it. Should those really behave differently in this regard? I'd love to learn about some compelling example of it being useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The optionality that was added to all elements since the omitted expression in the array literal breaks cases like this:

// @strict: true
// @exactOptionalPropertyTypes: true

function test3(arg: [number, undefined, string]) {}
test3([10, , "foo"]); // error but it should be OK

@@ -185,8 +197,6 @@ strictOptionalProperties1.ts(211,1): error TS2322: Type 'string | boolean | unde
const t2: [number, string?, boolean?] = [1, undefined];
~~
!!! error TS2322: Type '[number, undefined]' is not assignable to type '[number, string?, boolean?]'.
!!! error TS2322: Type at position 1 in source is not compatible with type at position 1 in target.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an elaboration got lost here, it's something I will investigate

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elaborations only occur the first time the error is generated - once that result is cached, the elaboration isn't copied to followup errors on the same types. In this case, now this comparison has the same types as the comparison with the error on line 10 of the file, so the elaboration is there now.

Copy link
Member

@weswigham weswigham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation-wise, this looks like a pretty solid improvement to me (reasoning about missing types in fewer places seems good to me), but I want to defer to @RyanCavanaugh for the final call on this one, since he probably has the most complete mental model for how exactOptionalPropertyTypes is supposed to work. And, specifically,

I'd rather prefer to explore this as a follow-up change in another PR - if there would be an intereset for it.

does sound interesting. It does, at least, seem like it'd help tie the optionality more strongly to the "property symbol" rather than the type, which is crucial for how exactOptionalPropertyTypes is supposed to be reasoned about (afaik).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Backlog Bug PRs that fix a backlog bug
Projects
Status: Waiting on reviewers
Development

Successfully merging this pull request may close these issues.

Tuple with optional element and exactOptionalPropertyTypes still returns undefined during iteration
5 participants