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

add types for set methods proposal #57230

Merged
merged 9 commits into from
May 16, 2024
Merged

Conversation

bakkot
Copy link
Contributor

@bakkot bakkot commented Jan 30, 2024

Fixes #57228.

Set methods are stage 3, and I expect to ask for stage 4 in April.

I've used the simplest possible types here, but in some sense these are too restrictive. It's perfectly reasonable to ask if a Set of 0 | 1 is a subset of a Set of number, for example, or even of a Set of 1 | 2 - anything where the intersection of the types is nonempty (I guess that would be, any type S such that T & S extends never is false).

Unfortunately I don't know of a good way to express that constraint, at least not without making the types way more complex. So I've stuck with the simple thing here. But if someone has a suggestion for better types I'd happily take it.

One possibility would be to have the Set-producing types take SetLike<T> but the predicates take SetLike<unknown>. I don't know if that would be better.

Do note that this this PR introduces the SetLike interface.

@typescript-bot typescript-bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Jan 30, 2024
@ssalbdivad
Copy link

I guess that would be, any type S such that T & S extends never is false

I think this concept of "overlaps with" or its opposite "is disjoint with" is actually useful quite often. For example, it would be the ideal type of the parameter in [].includes(...). Currently I use this utility:

export const includes = <array extends readonly unknown[], element>(
	array: array,
	element: element & array[number] extends never ? array[number] : element
): element is element & array[number] => array.includes(element)

declare const array: string[]
declare const overlapping: string | number
declare const nonOverlapping: boolean | number

// okay
includes(array, overlapping)

// error
includes(array, nonOverlapping)

I wonder if the team would consider adding a builtin Overlaps type for cases like these and the set methods in this PR.

@fatcerberus
Copy link

Unfortunately I don't know of a good way to express that constraint, at least not without making the types way more complex. So I've stuck with the simple thing here. But if someone has a suggestion for better types I'd happily take it.

I'm not sure there's a good solution for this right now - it's more or less the same problem as #48247/#26255 and the fix is generally considered to be #14520 (lower-bounded type parameters)--though FWIW I think an "overlaps with" constraint would be more useful for this than "supertype of"

@ssalbdivad
Copy link

@fatcerberus I definitely agree overlaps would be very useful in a number of circumstances like includes, as well as internally for a lot of these comparison errors that incorrectly assert the lack of an overlap (when really they're just checking for a downcast):

declare const a: { a: true }

declare const b: { b: true }

// '{ a: true; }' and '{ b: true; }' have no overlap
if (a === b) {
}

@fatcerberus
Copy link

Well, in reality any overlaps primitive we got would likely use the same internal check that produces the "these types don't overlap" error, so I don't think it would fix that problem by itself 😉

@ssalbdivad
Copy link

@fatcerberus Maybe it would be an opportunity to revisit that logic if it were to be published as its own utility 😅

@@ -9,3 +9,49 @@ interface MapConstructor {
keySelector: (item: T, index: number) => K,
): Map<K, T[]>;
}

interface SetLike<T> {
Copy link
Member

Choose a reason for hiding this comment

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

At first glance, this looks a lot like ReadonlySet. I know that these methods only need a subset of functionality, but it might be worth using that instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You need the keys method, so I don't think that works unless that gets added ReadonlySet as well. (And users shouldn't have to implement forEach for their own set-likes, though that isn't as big a deal since that probably doesn't come up as much.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wait, no, keys is already there, just in a different file, whoops.

Still, I think it would be kind of annoying not to be able to do set.union({ keys(){ return array.keys() }, size: array.size, has: () => {} }) or similar. Not the end of the world, but I'd prefer to keep SetLike unless there's a reason to switch to ReadonlySet.

@DanielRosenwasser
Copy link
Member

@typescript-bot pack this
@typescript-bot test this
@typescript-bot test top200
@typescript-bot user test this
@typescript-bot run dt
@typescript-bot perf test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 31, 2024

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 31, 2024

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 31, 2024

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

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 31, 2024

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

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 31, 2024

Heya @DanielRosenwasser, I've started to run the regular perf test suite on this PR at d9efe96. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jan 31, 2024

Hey @DanielRosenwasser, 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/159718/artifacts?artifactName=tgz&fileId=2BD1440F52B0CE1DB5E5082986F8EE22B10337C65AD4EDD247C29E5340509AFF02&fileName=/typescript-5.4.0-insiders.20240131.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/[email protected]".;

@DanielRosenwasser
Copy link
Member

The "overlaps" concept is what we internally call "comparability". The problem with expressing it as an intersection that produces never is that it is rarely possible to say an intersection of two object types is vacuous.

In other words, you can't say that { meow(): void } & { woof(): void } is an impossible type to fulfill, so there is no straightforward way to disallow catSet.intersection(dogSet).

@typescript-bot
Copy link
Collaborator

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

There were infrastructure failures potentially unrelated to your change:

  • 1 instance of "Package install failed"

Otherwise...

Something interesting changed - please have a look.

Details

puppeteer

packages/browsers/test/src/tsconfig.json

@typescript-bot
Copy link
Collaborator

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

@typescript-bot
Copy link
Collaborator

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

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Angular - node (v18.15.0, x64)
Memory used 295,662k (± 0.01%) 295,671k (± 0.01%) ~ 295,637k 295,725k p=0.810 n=6
Parse Time 2.66s (± 0.31%) 2.67s (± 0.19%) +0.01s (+ 0.38%) 2.66s 2.67s p=0.050 n=6
Bind Time 0.83s (± 1.52%) 0.82s (± 0.63%) ~ 0.82s 0.83s p=0.418 n=6
Check Time 8.19s (± 0.47%) 8.21s (± 0.34%) ~ 8.18s 8.26s p=0.870 n=6
Emit Time 7.10s (± 0.25%) 7.10s (± 0.23%) ~ 7.08s 7.12s p=1.000 n=6
Total Time 18.78s (± 0.30%) 18.80s (± 0.15%) ~ 18.78s 18.86s p=1.000 n=6
Compiler-Unions - node (v18.15.0, x64)
Memory used 193,514k (± 1.59%) 192,944k (± 1.25%) ~ 191,491k 197,365k p=0.689 n=6
Parse Time 1.35s (± 0.87%) 1.36s (± 1.76%) ~ 1.32s 1.38s p=0.220 n=6
Bind Time 0.72s (± 0.00%) 0.72s (± 0.00%) ~ 0.72s 0.72s p=1.000 n=6
Check Time 9.38s (± 0.50%) 9.41s (± 0.37%) ~ 9.36s 9.44s p=0.469 n=6
Emit Time 2.62s (± 0.66%) 2.61s (± 1.40%) ~ 2.55s 2.66s p=0.570 n=6
Total Time 14.08s (± 0.38%) 14.10s (± 0.37%) ~ 14.00s 14.14s p=0.332 n=6
Monaco - node (v18.15.0, x64)
Memory used 347,460k (± 0.00%) 347,465k (± 0.00%) ~ 347,442k 347,478k p=0.336 n=6
Parse Time 2.48s (± 0.44%) 2.48s (± 0.36%) ~ 2.47s 2.49s p=1.000 n=6
Bind Time 0.93s (± 0.68%) 0.93s (± 0.44%) ~ 0.92s 0.93s p=0.673 n=6
Check Time 6.96s (± 0.56%) 6.96s (± 0.55%) ~ 6.92s 7.03s p=1.000 n=6
Emit Time 4.07s (± 0.45%) 4.06s (± 0.34%) ~ 4.04s 4.08s p=0.241 n=6
Total Time 14.44s (± 0.36%) 14.43s (± 0.32%) ~ 14.38s 14.50s p=0.809 n=6
TFS - node (v18.15.0, x64)
Memory used 302,858k (± 0.01%) 302,858k (± 0.01%) ~ 302,832k 302,894k p=0.689 n=6
Parse Time 2.01s (± 0.86%) 2.02s (± 0.68%) ~ 1.99s 2.03s p=0.934 n=6
Bind Time 1.01s (± 1.04%) 1.00s (± 1.22%) ~ 0.99s 1.02s p=1.000 n=6
Check Time 6.33s (± 0.41%) 6.34s (± 0.34%) ~ 6.31s 6.37s p=0.624 n=6
Emit Time 3.59s (± 0.45%) 3.59s (± 0.42%) ~ 3.57s 3.61s p=1.000 n=6
Total Time 12.94s (± 0.21%) 12.96s (± 0.19%) ~ 12.92s 12.98s p=0.515 n=6
material-ui - node (v18.15.0, x64)
Memory used 511,313k (± 0.00%) 511,325k (± 0.01%) ~ 511,290k 511,384k p=0.748 n=6
Parse Time 2.65s (± 0.66%) 2.65s (± 0.98%) ~ 2.62s 2.68s p=0.935 n=6
Bind Time 0.99s (± 1.04%) 1.00s (± 0.98%) ~ 0.99s 1.01s p=0.203 n=6
Check Time 17.27s (± 0.37%) 17.28s (± 0.40%) ~ 17.19s 17.37s p=0.936 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 20.90s (± 0.26%) 20.92s (± 0.33%) ~ 20.83s 21.03s p=0.685 n=6
mui-docs - node (v18.15.0, x64)
Memory used 1,695,135k (± 0.00%) 1,695,118k (± 0.00%) ~ 1,695,074k 1,695,186k p=0.199 n=6
Parse Time 6.53s (± 0.29%) 6.52s (± 0.38%) ~ 6.49s 6.56s p=0.466 n=6
Bind Time 2.36s (± 0.22%) 2.36s (± 0.35%) ~ 2.34s 2.36s p=0.114 n=6
Check Time 55.43s (± 0.37%) 55.49s (± 0.33%) ~ 55.24s 55.66s p=0.687 n=6
Emit Time 0.16s (± 2.52%) 0.16s (± 2.52%) ~ 0.16s 0.17s p=1.000 n=6
Total Time 64.48s (± 0.34%) 64.53s (± 0.30%) ~ 64.26s 64.71s p=0.810 n=6
self-build-src - node (v18.15.0, x64)
Memory used 2,410,763k (± 0.04%) 2,410,667k (± 0.03%) ~ 2,409,469k 2,411,451k p=1.000 n=6
Parse Time 4.93s (± 0.99%) 4.94s (± 0.59%) ~ 4.89s 4.98s p=0.575 n=6
Bind Time 1.88s (± 0.27%) 1.88s (± 1.08%) ~ 1.87s 1.92s p=0.277 n=6
Check Time 33.45s (± 0.23%) 33.46s (± 0.35%) ~ 33.26s 33.58s p=0.810 n=6
Emit Time 2.69s (± 1.90%) 2.70s (± 1.91%) ~ 2.64s 2.78s p=0.810 n=6
Total Time 42.98s (± 0.17%) 42.99s (± 0.30%) ~ 42.76s 43.15s p=0.335 n=6
self-compiler - node (v18.15.0, x64)
Memory used 418,598k (± 0.00%) 418,659k (± 0.02%) ~ 418,588k 418,780k p=0.230 n=6
Parse Time 2.79s (± 2.23%) 2.74s (± 3.78%) ~ 2.66s 2.87s p=0.569 n=6
Bind Time 1.10s (± 5.44%) 1.17s (± 6.50%) ~ 1.07s 1.23s p=0.112 n=6
Check Time 15.10s (± 0.36%) 15.10s (± 0.34%) ~ 15.03s 15.16s p=1.000 n=6
Emit Time 1.14s (± 1.11%) 1.14s (± 1.29%) ~ 1.12s 1.16s p=0.869 n=6
Total Time 20.13s (± 0.27%) 20.15s (± 0.34%) ~ 20.07s 20.24s p=0.575 n=6
vscode - node (v18.15.0, x64)
Memory used 2,811,213k (± 0.00%) 2,811,228k (± 0.00%) ~ 2,811,192k 2,811,278k p=0.748 n=6
Parse Time 10.62s (± 0.38%) 10.61s (± 0.37%) ~ 10.56s 10.65s p=0.808 n=6
Bind Time 3.42s (± 0.55%) 3.41s (± 1.15%) ~ 3.39s 3.49s p=0.413 n=6
Check Time 59.81s (± 0.67%) 60.09s (± 0.57%) ~ 59.62s 60.54s p=0.378 n=6
Emit Time 16.14s (± 0.57%) 16.16s (± 0.62%) ~ 16.00s 16.29s p=0.628 n=6
Total Time 89.99s (± 0.38%) 90.27s (± 0.43%) ~ 89.72s 90.75s p=0.230 n=6
webpack - node (v18.15.0, x64)
Memory used 393,456k (± 0.01%) 393,471k (± 0.02%) ~ 393,354k 393,639k p=0.936 n=6
Parse Time 3.08s (± 0.65%) 3.07s (± 0.74%) ~ 3.04s 3.11s p=0.492 n=6
Bind Time 1.41s (± 0.39%) 1.39s (± 1.33%) ~ 1.37s 1.41s p=0.341 n=6
Check Time 14.00s (± 0.30%) 14.03s (± 0.34%) ~ 13.94s 14.06s p=0.171 n=6
Emit Time 0.00s (± 0.00%) 0.00s (±244.70%) ~ 0.00s 0.01s p=0.405 n=6
Total Time 18.48s (± 0.21%) 18.49s (± 0.20%) ~ 18.43s 18.52s p=0.627 n=6
xstate - node (v18.15.0, x64)
Memory used 513,374k (± 0.01%) 513,407k (± 0.00%) ~ 513,381k 513,432k p=0.065 n=6
Parse Time 3.27s (± 0.36%) 3.28s (± 0.30%) ~ 3.27s 3.29s p=0.314 n=6
Bind Time 1.54s (± 0.49%) 1.54s (± 0.49%) ~ 1.53s 1.55s p=1.000 n=6
Check Time 2.84s (± 0.87%) 2.85s (± 0.91%) ~ 2.81s 2.88s p=0.685 n=6
Emit Time 0.08s (± 4.99%) 0.08s (± 0.00%) ~ 0.08s 0.08s p=0.405 n=6
Total Time 7.73s (± 0.29%) 7.74s (± 0.47%) ~ 7.70s 7.79s p=0.518 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Angular - node (v18.15.0, x64)
  • Compiler-Unions - node (v18.15.0, x64)
  • Monaco - node (v18.15.0, x64)
  • TFS - node (v18.15.0, x64)
  • material-ui - node (v18.15.0, x64)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

tsserver

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-UnionsTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,345ms (± 0.43%) 2,342ms (± 0.75%) ~ 2,330ms 2,377ms p=0.227 n=6
Req 2 - geterr 5,546ms (± 1.36%) 5,548ms (± 1.54%) ~ 5,419ms 5,620ms p=0.936 n=6
Req 3 - references 323ms (± 0.30%) 325ms (± 1.56%) ~ 321ms 332ms p=0.742 n=6
Req 4 - navto 274ms (± 1.45%) 275ms (± 1.32%) ~ 271ms 279ms p=1.000 n=6
Req 5 - completionInfo count 1,356 (± 0.00%) 1,356 (± 0.00%) ~ 1,356 1,356 p=1.000 n=6
Req 5 - completionInfo 87ms (± 8.43%) 90ms (± 7.40%) ~ 84ms 101ms p=0.466 n=6
CompilerTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,479ms (± 1.56%) 2,483ms (± 1.23%) ~ 2,448ms 2,535ms p=0.689 n=6
Req 2 - geterr 4,197ms (± 2.07%) 4,181ms (± 1.78%) ~ 4,123ms 4,279ms p=0.630 n=6
Req 3 - references 336ms (± 1.77%) 340ms (± 1.61%) ~ 331ms 346ms p=0.568 n=6
Req 4 - navto 284ms (± 0.53%) 285ms (± 1.19%) ~ 281ms 291ms p=0.466 n=6
Req 5 - completionInfo count 1,518 (± 0.00%) 1,518 (± 0.00%) ~ 1,518 1,518 p=1.000 n=6
Req 5 - completionInfo 80ms (± 6.65%) 85ms (± 8.23%) ~ 74ms 90ms p=0.106 n=6
xstateTSServer - node (v18.15.0, x64)
Req 1 - updateOpen 2,598ms (± 0.57%) 2,612ms (± 0.44%) ~ 2,594ms 2,626ms p=0.126 n=6
Req 2 - geterr 1,736ms (± 2.23%) 1,731ms (± 2.70%) ~ 1,680ms 1,783ms p=1.000 n=6
Req 3 - references 116ms (± 9.63%) 119ms (± 8.24%) ~ 107ms 128ms p=0.466 n=6
Req 4 - navto 370ms (± 0.41%) 371ms (± 0.56%) ~ 369ms 375ms p=1.000 n=6
Req 5 - completionInfo count 2,078 (± 0.00%) 2,078 (± 0.00%) ~ 2,078 2,078 p=1.000 n=6
Req 5 - completionInfo 311ms (± 1.57%) 311ms (± 2.40%) ~ 301ms 319ms p=0.873 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • CompilerTSServer - node (v18.15.0, x64)
  • Compiler-UnionsTSServer - node (v18.15.0, x64)
  • xstateTSServer - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

startup

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
tsc-startup - node (v18.15.0, x64)
Execution time 153.16ms (± 0.20%) 152.79ms (± 0.18%) -0.37ms (- 0.24%) 151.87ms 157.17ms p=0.000 n=600
tsserver-startup - node (v18.15.0, x64)
Execution time 229.03ms (± 0.16%) 228.91ms (± 0.15%) -0.12ms (- 0.05%) 227.67ms 232.49ms p=0.000 n=600
tsserverlibrary-startup - node (v18.15.0, x64)
Execution time 231.42ms (± 0.19%) 231.30ms (± 0.18%) -0.12ms (- 0.05%) 229.33ms 238.19ms p=0.031 n=600
typescript-startup - node (v18.15.0, x64)
Execution time 230.78ms (± 0.19%) 230.77ms (± 0.18%) ~ 229.27ms 236.83ms p=0.987 n=600
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • tsc-startup - node (v18.15.0, x64)
  • tsserver-startup - node (v18.15.0, x64)
  • tsserverlibrary-startup - node (v18.15.0, x64)
  • typescript-startup - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@typescript-bot
Copy link
Collaborator

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

Everything looks good!

@ssalbdivad
Copy link

@DanielRosenwasser Yes, that is exactly the behavior I'd expect in a structural type system, with a result of Set<{meow(): void; woof(): void}>. The type could still give completions based on the base type which must be overlapped, but this kind of intersection is definitely sensible as illustrated by an alternate example like { poisonous: true } & { mammalian: true } for a set of platypuses and echidnas 😄

I'm often frustrated by TS overreaching based on a comparability check (e.g. #44645), especially when the word "overlap" is used in the error message which has a specific meaning that does not match the criteria for the error.

@DanielRosenwasser
Copy link
Member

I brought this up in the design meeting, specifically focusing on

  1. the types of union/intersection
  2. the parameter container types.

The feedback for them was:

  1. Methods like union and intersection should be generic and return a corresponding TypeScript type. union should return a Set<T | U>, and intersection should return a Set<T & U>.

  2. Methods should not expect their arguments to be related to the underlying container type. This risks accidental invocations of intersection that are guaranteed to produce empty collections, but it has the benefit of allowing more symmetric operations.

    In other words, both stringSet.intersection(stringOrBooleanSet) and stringOrBooleanSet.intersection(stringSet) are valid invocations of each other.

    I'll note that I am personally the most opposed to this behavior, but I was the lone dissenter. 🙂

  3. With respect to SetLike, @rbuckton raised the same points of ReadonlySet being overkill. I suggested ReadonlySetLike might make more sense. Nobody else had a strong opinion.

@bakkot, in this PR I'd suggest to just address the first two points, do whatever you feel is right for point 3, and we will bikeshed by the time we can bring this in for TypeScript 5.5.

@bakkot
Copy link
Contributor Author

bakkot commented Jan 31, 2024

WFM, thanks for the feedback. I'll get that pushed up later today.

Any opinions on isSubsetOf and friends? If not I'll default to making them generic, following point 2, even though (because they return booleans) this doesn't let you constrain the output.

@DanielRosenwasser
Copy link
Member

Any opinions on isSubsetOf and friends? If not I'll default to making them generic

I think the right thing for those is to take a Set<unknown> since the type parameter will never be observed.

@fatcerberus
Copy link

In other words, both stringSet.intersection(stringOrBooleanSet) and stringOrBooleanSet.intersection(stringSet) are valid invocations of each other.

This is generally what people expect of e.g. Array.prototype.includes as well, and I largely concur, but to my knowledge there's no way to allow that without also allowing stringSet.intersection(numberSet) (unless we get lower-bounded or comparability constraints), correct?

@inoyakaigor
Copy link

@bakkot I'm not from TS team and just curious: wouldn't it be better if ReadonlySetLike interface will be like this:

type ReadonlySetLike<T> = Pick<Set<T>, 'size' | 'has' | 'keys'>

In any case of probable future changes of Set typings for size, has or keys ReadonlySetLike will also have it automatically

@bakkot
Copy link
Contributor Author

bakkot commented Apr 15, 2024

Sure, that would work too. I leave it up to the TS to decide which approach they'd prefer.

@noinkling
Copy link

noinkling commented Apr 16, 2024

Doesn't keys() need to return an Iterator anyway, not an Iterable?

keys(): Iterable<T>;

In current Chrome:

const mySetLike = {
  size: 2,
  has(a) { return a === 'foo' || a === 'bar' },
  keys() { return ['foo','bar'] }
};

// new Set(['baz']).union(mySetLike);
// Uncaught TypeError: string "next" is not a function

mySetLike.keys = () => {
  let i = 0;
  return {
    next() {
      i++;
      if (i === 1) return { value: 'foo', done: false };
      if (i === 2) return { value: 'bar', done: false };
      return { done: true };
    }
  };
};

new Set(['baz']).union(mySetLike);
// Set(3) {'baz', 'foo', 'bar'}

Piggybacking off Set/ReadonlySet isn't right either though because it would require keys() to return an IterableIterator, which isn't necessary (as the example shows).

@bakkot
Copy link
Contributor Author

bakkot commented Apr 16, 2024

@noinkling Nice catch, that's an embarrassing typo given that I designed the feature 😅. Fixed.

Possibly should update the description of keys/values/entries on Map and Set to say "iterable iterator" instead of "iterable", but not in this PR.

@inoyakaigor
Copy link

inoyakaigor commented May 2, 2024

@bakkot One more question about return type for difference and symmetricDifference.
I suspect a return type for this two functions should be something like this, isn't it?

difference<U>(otherSet: ReadonlySetLike<U>): Set<Exclude<T, U>> 

symmetricDifference<U>(otherSet: ReadonlySetLike<U>): Set<Exclude<T, U> & Exclude<U, T>>

@bakkot
Copy link
Contributor Author

bakkot commented May 2, 2024

Depends on how we want to make the tradeoff between precision and making the types noisier, but yes, formally that's correct. Wrong, see below.

@inoyakaigor
Copy link

Any blockers* for this PR? My PR into Mobx mobxjs/mobx#3853 state manager (which mostly used for React.js) is depending on it


* except failed tests

Copy link
Member

@rbuckton rbuckton left a comment

Choose a reason for hiding this comment

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

It looks like baselines need to be updated after the most recent commit.

@rbuckton
Copy link
Member

@bakkot One more question about return type for difference and symmetricDifference. I suspect a return type for this two functions should be something like this, isn't it?

difference<U>(otherSet: ReadonlySetLike<U>): Set<Exclude<T, U>> 

symmetricDifference<U>(otherSet: ReadonlySetLike<U>): Set<Exclude<T, U> & Exclude<U, T>>

Those definitions would be too strict since a given instantiation of Set does not imply that the set itself contains all of those values, only that it may contain those values:

const set1 = new Set<"A" | "B">(["A", "B"]);
const set2 = new Set<"B" | "C">(["C"]);
const set3 = set1.difference(set2);
set.has("A"); // true
set.has("B"); // true

Even though set2 is declared as Set<"B" | "C">, it does not actually contain a "B" value, thus "B" is not actually excluded from set3.

@rbuckton
Copy link
Member

@bakkot I'm not from TS team and just curious: wouldn't it be better if ReadonlySetLike interface will be like this:

type ReadonlySetLike<T> = Pick<Set<T>, 'size' | 'has' | 'keys'>

In any case of probable future changes of Set typings for size, has or keys ReadonlySetLike will also have it automatically

IMO, an interface is a better option as it allows for interface merging if a polyfill is loaded that also introduces these types.

@inoyakaigor
Copy link

Those definitions would be too strict

@rbuckton so maybe wrap it in Partial<> ?

Comment on lines +48 to +52
isSubsetOf(other: ReadonlySetLike<unknown>): boolean;
/**
* @returns a boolean indicating whether all the elements in the argument are also in this Set.
*/
isSupersetOf(other: ReadonlySetLike<unknown>): boolean;
Copy link
Member

Choose a reason for hiding this comment

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

@DanielRosenwasser would overloads like the following be worthwhile?

interface Set<T> {
  isSubsetOf<U>(other: ReadonlySetLike<U>): this is Set<T & U>;
  isSupersetOf<U>(other: ReadonlySetLike<U>): other is ReadonlySetLike<T & U>;
}

Copy link
Member

Choose a reason for hiding this comment

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

We can always change these in a follow-up PR.

src/lib/esnext.collection.d.ts Show resolved Hide resolved
@rbuckton
Copy link
Member

Those definitions would be too strict

@rbuckton so maybe wrap it in Partial<> ?

Partial does not address the concern I raised. A Partial<"B"> is still "B". The only way you could safely exclude any types would be if the argument was a fixed-length tuple type containing only non-union literal types:

const set1 = new Set<"A" | "B">(["A", "B"]);
const set2 = set1.difference(["B", "C"]); // infer the type `["B", "C"]` here

However, difference does not accept array-like values and there is no concept of a read-only literal set in either JS or TS.

@bakkot
Copy link
Contributor Author

bakkot commented May 16, 2024

It looks like baselines need to be updated after the most recent commit.

Hm. The way I've been doing that (npm ci; npm run build; npm run test -- --no-lint; npx hereby baseline-accept) doesn't produce any changes locally for this commit. And the diff in CI doesn't immediately look like it should be caused by the last commit, even though the one before was passing. Is there a different process I should follow here?

@jakebailey
Copy link
Member

CI always tests main + the PR; have you merged main locally?

@bakkot
Copy link
Contributor Author

bakkot commented May 16, 2024

Aha, thanks, that's fixed it.

Comment on lines +48 to +52
isSubsetOf(other: ReadonlySetLike<unknown>): boolean;
/**
* @returns a boolean indicating whether all the elements in the argument are also in this Set.
*/
isSupersetOf(other: ReadonlySetLike<unknown>): boolean;
Copy link
Member

Choose a reason for hiding this comment

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

We can always change these in a follow-up PR.

@rbuckton rbuckton merged commit e8274f7 into microsoft:main May 16, 2024
28 checks passed
@bakkot bakkot deleted the set-methods branch May 20, 2024 23:39
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
None yet
Development

Successfully merging this pull request may close these issues.

Add types for stage 3 "set methods" proposal
10 participants