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

Fix typescript types for typescript 2.4 #1285

Conversation

sandersn
Copy link

@sandersn sandersn commented Aug 9, 2017

Typescript does a lot more analysis than previous versions. This (1) exposed some bugs in the types and (2) caused the compiler to run out of memory and crash. The crash, and many of the new checks, can be avoided by passing --noStrictGenericChecks but users of immutable shouldn't be required to do that.

Improved Checks

  1. The return type of Stack.flatMap's mapper was incorrectly M but should be Iterable<M>.
  2. Set and related types should extend Collection<never, T> not Collection<T, T>. That's because Collection.map (and flatMap and filter) assert that the returned collection has the same key type and a different value type. But Set.map returns Set<T>, which, if it extends Collection<T, T> changes both the key type and value type. Changing to Set<T> extends Collection<never, T> means that the key type never changes.

Workarounds

  1. Map and related types have a flatMap method that is not a valid Collection.flatMap implementation because it changes the key type and value type, much like Set (see flatMap typescript type for Keyed, [Ordered]Map are not substitutable for Collection.flatMap #1283). However, here the implementation and the type are wrong, so I worked around the problem by letting Map.flatMap return Map<any, any> instead of Map<KM, VM> (the previous type) or Map<K, M> (a valid subtype of Collection.flatMap).
  2. (Fixed in Typescript 2.5.3) In Typescript 2.4, the compiler spends too much time and memory checking the extends relationships between Collection and its subtypes. For example, OrderedMap extends Map extends Collection.Keyed extends Collection. The worst offender is toSeq since it refers to a subtype, Seq, which is then extended multiple times by further subtypes. This makes checking goes around in circles multiple times for each subtype. I chose to rely on upcoming optimisations in Typescript 2.5.3 that will short-circuit most of these cycles.

Both workarounds have multiple possible fixes; for example, workaround (1) could instead declare two overloads of flatMap, one with the Collection-compatible type and one with the current type. Workaround (2) could delete toSeq entirely from Collection, or change the return type of toSeq to any in Collection, or change the return type of toSeq in subtypes of Collection.

`Keyed.flatMap` says that `mapper` is allowed to change the key type and the
value type, but `Collection.keyMap` says that `mapper` is only allowed to
change the value type. This commit resolves the typing problem by making
the return type's type arguments both `any`. It does not resolve the
mismatch in the implementation of Map.flatMap, for example.
M should actually be Iterable<M> since this is map, not flatMap.
The previous type, Set<T> extends Collection<T, T>, results in an
incorrect type for `map` because `map` is not allowed to change the key
type, only the value type. But map's previous return type of Set<T>
asserted that both the key *and* the value type changed. Now the key
type is always `never`.
This works around the out-of-memory crash in Typescript 2.4 that arises
from the compiler doing too much work when checking the correctness of
`extends` relationships. The Typescript compiler got caught in a deep
recursion when checking `toSeq: Seq<K, V>`, the previous type.
@facebook-github-bot
Copy link

Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign up at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need the corporate CLA signed.

If you have received this in error or have any questions, please contact us at cla@fb.com. Thanks!

@facebook-github-bot
Copy link

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@samwgoldman
Copy link
Contributor

We noticed the issue with Set and map, flatMap as well. (See #879)

Some immutable tooling doesn't understand never yet.
@sandersn
Copy link
Author

sandersn commented Aug 9, 2017

Looks like some doc-generation tooling doesn't understand never yet, so I changed Set to extend Collection<void, T> instead.

Some immutable tooling doesn't understand void or never.
@sandersn
Copy link
Author

sandersn commented Aug 9, 2017

The failing tests points out an additional bug with the definition of Set.has, which for other collections checks the key but for sets checks for the value.

I'm still working on running tests locally, but I don't know how to copy type-definitions/Immutable.d.ts to dist/immutable-nonambient.d.ts, which is what the tests refer to. I poked around the gulpfile and didn't find anything? Is this manual?

@sandersn
Copy link
Author

sandersn commented Aug 9, 2017

Never mind, I eventually got it by running through the tasks in package.json

Set.has shouldn't be used; it refers to the key type of the collection
and the key type of Set is always number. Compare indexed types like
List, where this is also true.
1. Set<T> once again extends Collection<never, T>.
2. pages/lib/TypeKind.js understands Never now.
3. Remove calls to Set.has and Set.get. These refer to the key type,
which is now `never`. Set.contains should be used instead of Set.has.
`if (Set.contains(x)) Set.get(x)` should be replaced with
`if (Set.contains(x)) x` since Sets shouldn't change the identity of
their items.
@emmanueltouzery
Copy link
Contributor

emmanueltouzery commented Aug 26, 2017

this could be totally the wrong place to report this (or maybe not?) but I tried to port my immutables‑using typescript app to typescript@2.6.0-dev.20170826 and the immutables code from this pull request, and I get an unexpected behaviour.

See my commit:
emmanueltouzery/ng-typeview@86ab9e6

I would have thought the extra cast I added to make it build wouldn't be needed? Without it, it appears to infer type {} for Map<string,string>.get(string), instead of string|undefined and I have no idea why.

That cast was not needed with tsc 2.3 and the previous immutables TS bindings (tsc goes out of memory on this code with both 2.4.2 and 2.5RC).

Previously it was any to work around Typescript 2.4's out-of-memory
crash.
@sandersn
Copy link
Author

sandersn commented Sep 5, 2017

@emmanueltouzery the problem is that controllerNameToFilename: Map<{}, {}>. If you manually annotate it as Map<string, string>, the compiler error goes away. I'll check why the call to the map constructor doesn't infer the right type arguments.

@sandersn
Copy link
Author

sandersn commented Sep 5, 2017

I tracked the problem back to the preceding declaration:

    const controllerNameToFilename =
        Map(
            viewInfos
                .filter(vi => vi.controllerName.isSome())
			          // JS files are not going to have a scope interface
			          // definition so they're not helpful. Also, we can
			          // get twice the same file: original TS & compiled JS.
			          // => keep only the original TS in that case.
			          .filter(vi => vi.fileName.toLowerCase().endsWith(".ts"))
                .map(vi => [vi.controllerName.some(), vi.fileName] as [string, string]));

Notice that I added as [string, string] there at the end. Without it, the return type is inferred as string[], and the wrong overload of map gets chosen, and controllerNameToFilename gets the type Map<{}, {}> instead of Map<string, string>. I'm not sure why this worked in previous versions of immutable, though.

@leebyron
Copy link
Collaborator

Happy to see work here, but some feedback:

  • I think all of the changes with key: never aren't correct where previously they were correct.
  • Ideally we can avoid returning any types. What we did for better flow support was just add explicit overrides of methods on the final concrete collection types.

@sandersn
Copy link
Author

Here's an example of where code goes wrong without key: never:

let nset = Immutable.Set.of(1,2,3,4)
let ncol: Immutable.Collection<number, number> = nset;
nset.map(v => v.toString()).map((v, k) => k.length); 
ncol.map(v => v.toString()).map((v, k) => k.length); 

The last two lines produces Set { 1 } at runtime, but the last line has a type error because map(v => v.toString()) produces a Collection<number, string> not Collection<string, string>, so the types say that k: number in map((v, k) => k.length)

With key: never you are prevented from using the key with sets. You have to use the value instead. Both lines have a type error and have to change the second map to map(v => v.length).

I understand this is a break for Typescript 2.3 code that uses the keys of a Set today, so if you have guidance I would appreciate it. I'm still thinking about the second case; there might be a way to avoid breaks there.

@leebyron
Copy link
Collaborator

@sandersn thanks for the example, I think perhaps this would be better solved by a more specific type for map() that's aware of a new key type as well on Set (and SeqSet) rather than blocking the use of keys

Copy link
Collaborator

@leebyron leebyron left a comment

Choose a reason for hiding this comment

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

.

@sandersn
Copy link
Author

OK, I restored the original behaviour for subclasses. I had to do this by adding overloads in the base classes (Seq, Collection, etc) to account for the inconsistent types coming from Set and Dictionary. Instead of a single more specific type, this allows overrides to override one of two different types.

Most of these overloads are reasonable, except for Collection.map, because Set.map has identical parameter types to Collection.map, just a different return type. So the compiler can't distinguish them and always chooses the first one. There I used a hack suggested by @DanielRosenwasser, which is a variadic function (...args: never[]): any. Anything is assignable to this, but it won't be called by mistake because values with type never are so rare.

@sandersn
Copy link
Author

Also, overloads are not inherited, so only uses of the base Collection interface will see the bogus overload. Most people will only ever see the methods that go with the collection type they're using.

leebyron pushed a commit that referenced this pull request Sep 30, 2017
Squashed commit of the following:

commit d87e7756aa0310136436452cb2e783b7b7134f39
Author: Travis CI <github@fb.com>
Date:   Fri Sep 29 18:47:46 2017 -0700

    Revert changes to tests that seem unnecessary, update package.json typescript to 2.5 to properly run tests and prove no deadlock

commit 55d6027ce92c761bceae825830678dec9c84fb95
Merge: 5381bfc 041cc87
Author: Travis CI <github@fb.com>
Date:   Fri Sep 29 18:12:58 2017 -0700

    Merge branch 'fix-typescript-types-for-typescript-2.4' of https://github.com/sandersn/immutable-js into sandersn-fix-typescript-types-for-typescript-2.4

commit 041cc87
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Fri Sep 29 13:50:09 2017 -0700

    Update dist and ts-tests

commit c83e64c
Merge: f393a68 37195e5
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Fri Sep 29 13:42:09 2017 -0700

    Merge branch 'master' into fix-typescript-types-for-typescript-2.4

commit f393a68
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Fri Sep 29 13:40:54 2017 -0700

    Update dist files

commit 2e7576e
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Fri Sep 29 13:39:37 2017 -0700

    Overloads allow inconsistent Collection subclasses

commit 482d419
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Tue Sep 5 11:19:45 2017 -0700

    Return Collection.toSeq's origin type: Seq<K, V>

    Previously it was any to work around Typescript 2.4's out-of-memory
    crash.

commit b9ae2bd
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Wed Aug 9 13:09:57 2017 -0700

    Change Set key type back to never

    1. Set<T> once again extends Collection<never, T>.
    2. pages/lib/TypeKind.js understands Never now.
    3. Remove calls to Set.has and Set.get. These refer to the key type,
    which is now `never`. Set.contains should be used instead of Set.has.
    `if (Set.contains(x)) Set.get(x)` should be replaced with
    `if (Set.contains(x)) x` since Sets shouldn't change the identity of
    their items.

commit ea7ffae
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Wed Aug 9 12:52:26 2017 -0700

    Update .d.ts files in dist/

commit af305ec
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Wed Aug 9 12:49:48 2017 -0700

    Tests:change Set.has->Set.contains

    Set.has shouldn't be used; it refers to the key type of the collection
    and the key type of Set is always number. Compare indexed types like
    List, where this is also true.

commit adb9326
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Wed Aug 9 10:59:14 2017 -0700

    Set<T> extends Collection<number, T>, not never

    Some immutable tooling doesn't understand void or never.

commit f9c94f9
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Wed Aug 9 10:53:34 2017 -0700

    Set<T> extends Collection<void, T>, not never

    Some immutable tooling doesn't understand never yet.

commit 96b81ea
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Wed Aug 9 09:56:49 2017 -0700

    Work around TS2.4:Collection.toSeq now returns any

    This works around the out-of-memory crash in Typescript 2.4 that arises
    from the compiler doing too much work when checking the correctness of
    `extends` relationships. The Typescript compiler got caught in a deep
    recursion when checking `toSeq: Seq<K, V>`, the previous type.

commit 9b0eb51
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Wed Aug 9 09:51:27 2017 -0700

    Make OrderedSet's [flat]map/filter have key:never

    Missed in previous commit

commit e7d7ec9
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Wed Aug 9 09:35:16 2017 -0700

    Set types:K type parameter is now never

    The previous type, Set<T> extends Collection<T, T>, results in an
    incorrect type for `map` because `map` is not allowed to change the key
    type, only the value type. But map's previous return type of Set<T>
    asserted that both the key *and* the value type changed. Now the key
    type is always `never`.

commit 60fc276
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Wed Aug 9 09:32:15 2017 -0700

    Return Stack.flatMap's mapper's return type

    M should actually be Iterable<M> since this is map, not flatMap.

commit c750da4
Author: Nathan Shively-Sanders <nathansa@microsoft.com>
Date:   Tue Aug 8 16:25:55 2017 -0700

    Loosen type of Keyed.flatMap and descendants

    `Keyed.flatMap` says that `mapper` is allowed to change the key type and the
    value type, but `Collection.keyMap` says that `mapper` is only allowed to
    change the value type. This commit resolves the typing problem by making
    the return type's type arguments both `any`. It does not resolve the
    mismatch in the implementation of Map.flatMap, for example.
@leebyron
Copy link
Collaborator

Thanks for your help on this. I amended a few changes in the process of merging this, please let me know if any follow ups are necessary

@leebyron leebyron closed this Sep 30, 2017
errendir added a commit to errendir/immutable-js that referenced this pull request Sep 30, 2017
…ings

* facebook/master: (48 commits)
  4.0.0-rc.3
  Use latest immutable.js build on website
  Only build docs on tagged releases (immutable-js#1321)
  Relicense as MIT (immutable-js#1320)
  Fix rendering issue on tall screens
  Merge immutable-js#1285
  Do not throw from hasIn (immutable-js#1319)
  Add more flow tests for Records
  Upgrade prettier and jest (immutable-js#1318)
  Merge immutable-js#1193
  Minor flow record fixes
  Merges immutable-js#1195
  fixed immutable-js#1290 by removing relative position from star count (immutable-js#1317)
  Fix deploy script
  Adds a script which automatically builds a #npm branch (immutable-js#1316)
  Use .github directory
  Merges immutable-js#1314
  Flow config ignore all node_modules
  Update to latest version of flow (immutable-js#1312)
  fixed immutable-js#1313 by fixing two borked reducers (immutable-js#1315)
  ...
@sandersn
Copy link
Author

sandersn commented Oct 2, 2017

Nope, looks good. I forgot about the test changes for Set that no longer needed to be changed.

@sandersn sandersn deleted the fix-typescript-types-for-typescript-2.4 branch May 8, 2019 03:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants