Skip to content

[@types/underscore] More comprehensive tests for Underscore collection and array functions + definition changes as appropriate#45201

Closed
reubenrybnik wants to merge 130 commits intoDefinitelyTyped:masterfrom
reubenrybnik:chain-fixes-and-tests
Closed

[@types/underscore] More comprehensive tests for Underscore collection and array functions + definition changes as appropriate#45201
reubenrybnik wants to merge 130 commits intoDefinitelyTyped:masterfrom
reubenrybnik:chain-fixes-and-tests

Conversation

@reubenrybnik
Copy link
Copy Markdown
Contributor

  • Use a meaningful title for the pull request. Include the name of the package modified.
  • Test the change in your own code. (Compile and run.)
  • Add or edit tests to reflect the change. (Run with npm test.)
  • Follow the advice from the readme.
  • Avoid common mistakes.
  • Run npm run lint package-name (or tsc if no tslint.json is present).
  • Provide a URL to documentation or source code which provides context for the suggested changes: https://underscorejs.org/
  • If this PR brings the type definitions up to date with a new version of the JS library, update the version number in the header.
  • Include tests for your changes
  • If you are making substantial changes, consider adding a tslint.json containing { "extends": "dtslint/dt.json" }. If for reason the any rule need to be disabled, disable it for that line using // tslint:disable-next-line [ruleName] and not for whole package so that the need for disabling can be reviewed.

Changes in this PR include:

  • Adding a standardized set of tests for all Underscore Collection and Array functions similar to those originally added for Find
  • Updating more Collection functions to also allow for Dictionaries and ObjectIterators
  • Updating functions to support more of the Iteratee partial object and property name overloads where appropriate
  • Updating some return types to provide more accurate results including adding the possibility of undefined for some items that result in single values, a tuple for partition, and arrays instead of _.Lists where appropriate
  • Removing some function-level generics in Underscore and _Chain that can get in the way of correct typing being inherited from the generics on those interfaces
  • Referencing the primary function type definitions for aliases instead of duplicating them
  • Replacing _ChainOfArrays with conditional type logic that provides better results for flatten outside of doing a prior chained map
  • Updating the definition of Collection since the current empty interface doesn't provide much value
  • Updating a few type-guard-type functions to provide type guard results
  • Adding myself to the set of maintainers for Underscore type definitions

Changes I'm considering including in this PR but am not certain of include:

  • Updating the max and min overloads that take iterators to result in T | number since calling it on an empty collection will result in -Infinity
  • Forcing the iterators for max and min to always return numbers since returning other types doesn't seem to be useful
  • Making the return type of _Chain.chain "never" or removing chain from _Chain altogether since every experiment I tried with it resulted in a stack overflow
  • Using $ExpectType in more places
  • Adding actual JS assertions of some sort to tests and getting those to run as part of npm run tests somehow
  • Updating most of the collection parameters in existing functions to be readonly per recommendations in Common Mistakes

Additional potentially breaking changes that I'm considering making in future PRs that I could instead work on including in this one if that would be preferable include:

  • Adding tests and possibly updating definitions for at least some Object functions
  • Splitting _Chain into a set of items like _ChainList, _ChainDictionary, _ChainObject, and maybe _ChainString
  • Adding some Object functions to _ChainSingle

I work for a company that uses underscore fairly extensively in its codebase. Recently we looked into upgrading from 1.8.1 definitions to 1.9.4 but ran into some issues similar to those fixed in #36319, #36510, and #40417. I had originally planned on just fixing the additional issues as well, but when adding tests for those fixes I thought it might be more useful to add tests for more functions than just those that I was changing, and while adding tests I found several more changes that it seemed useful to make. I had originally hoped to create an initial PR that didn't include breaking changes (except for fixing inaccuracies that would have required definition consumers to use an assertion to get correctly running JS) and a subsequent one that did, but unfortunately that didn't end up being feasible. If it would be useful for me to split this up or reduce the set of changes made in some way, let me know.

@regevbr it looks like you may have been working on similar changes a while back as indicated by comments in #36319 and #36510 (which admittedly I wish I would have looked more closely at before today); if it would make more sense to do so, feel free to either incorporate some or all of these changes and/or tests into the changes you were planning to make instead or have me decline this PR in favor of similar changes that may be coming soon.

This includes several breaking changes to existing signatures, but I'm not sure whether it would be best to change the version here to 1.10 or 2.0 or fork these to a v2 of definitions or even possibly do nothing. 1.10 is at least a valid version of Underscore and doesn't appear to have added new functionality that would involve changes to typings, so it's probably the best option if these shouldn't be forked.

From the handbook, I did update code to use "any" in a few places where the return value of a callback parameter isn't useful to the parameter or return types of the function (e.g. sorting an array or determining uniqueness of items using the returned value of a callback) and also continued to use it in a few return types (see e.g. commit 8480687). I would have used "unknown" instead, but at this time I don't need to push the supported version up to 3.0. I'd personally prefer to keep it at or below the current lowest supported version of 2.9, but I'd be happy to bump it up if others feel differently.

mness-cc added 30 commits May 3, 2020 23:58
… objects to also accept an ObjectIterator and combining similar definitions for map and collect to be consistent with these and other existing definitions like find and detect.
…yield Dictionaries instead of Lists since _(dict).each(...) and _(dict).forEach(...) both yield the original dict object that was passed in.
…I just realized that the technically incorrect _Chain<T> return type actually worked exactly like _ChainSingle<T> from the perspective of a .value() call, which means that adding undefined would technically be a breaking change of a specific call chain.

Also removing some copy pasta in a few test namespaces.
…above the List version since otherwise List will always win.
… or chain what they are wrapping rather than T[].
…foldr can take a MemoObjectIterator as well as a MemoIterator.
… aren't already and updating them to refer to the correct primary function on UnderscoreStatic.
…opping overloads that aren't super useful since any covers everything.
@reubenrybnik
Copy link
Copy Markdown
Contributor Author

reubenrybnik commented Jul 24, 2020

@jgonggrijp I've prepped the Round 3c changes you requested for mapObject and findKey (with pairs tossed in) in commit
a62a3ed.

Actually, I wouldn't rule out the possibility that future iteratee shorthands produce two- or three-argument functions. That said, it is true that none of them currently does, so we could change this again when needed.

Then again, I'm not sure whether restricting the number of parameters to one actually solves anything. I guess the second and third get in the way if you want to call a matcher or a property accessor standalone with a single argument, but maybe those parameters should be optional on CollectionIteratee, anyway.

These parameters are definitely always supplied by Underscore, so I don't think making them optional in the type used by Underscore functions would be the right move since it would require the addition of unnecessary type guards when trying to use their values. I guess it would probably be best to have them return a full CollectionIteratee and possibly switch to a reduced interface if folks end up unhappy about that.

@jgonggrijp
Copy link
Copy Markdown
Contributor

These parameters are definitely always supplied by Underscore, so I don't think making them optional in the type used by Underscore functions would be the right move since it would require the addition of unnecessary type guards when trying to use their values.

Wait, I'm not following you anymore. Where would these typeguards have to be added if we were to make the second and third argument of CollectionIterator optional?

I guess it would probably be best to have them return a full CollectionIteratee and possibly switch to a reduced interface if folks end up unhappy about that.

By "them", do you mean matcher et al? That wouldn't be correct.

@reubenrybnik
Copy link
Copy Markdown
Contributor Author

Wait, I'm not following you anymore. Where would these typeguards have to be added if we were to make the second and third argument of CollectionIterator optional?

In the callback code of any function that uses CollectionIterator; see the example below.

interface CollectionIterator<T extends TypeOfCollection<V>, TResult, V = Collection<T>> {
    (element: T, key?: CollectionKey<V>, collection?: V): TResult;
}

// admittedly not the best example code
_.find({ a: 'one', b: 'two' }, (element, key, collection) => {
    if (key.length > 3) { // error: key is possibly 'undefined'
        return true;
    }

    if (collection.a === element) { // error: collection is possibly 'undefined'
        return true;
    }

    return false;
});

By "them", do you mean matcher et al? That wouldn't be correct.

It would not reflect reality as well as it could, but it would make it more future-proof. I'm fine with making a separate one-parameter or optional-parameter version of CollectionIterator if you'd prefer, though (and personally I'm leaning toward one-parameter over optional-parameter but it's not a strong preference).

@jgonggrijp
Copy link
Copy Markdown
Contributor

jgonggrijp commented Jul 24, 2020

How did you find out that the type checker would behave in the way that you described in your code example? I find it very counterintuitive. I would expect the find signature to restrict what types of functions are allowed to be passed, while the parameter list of the function itself (the arrow function in this case) determines the actual type of the function, and the site of invocation of the function (i.e., somewhere buried in the function body of find) to be the locus where TS would check whether the caller is passing the right types of arguments. The latter check is never applied in this case because Underscore itself is plain JS.

As for matcher et al returning a CollectionIterator with three required parameters: that wouldn't just be slightly off. It would break the code of anyone who invokes a matcher with only one argument.

@reubenrybnik
Copy link
Copy Markdown
Contributor Author

How did you find out that the type checker would behave in the way that you described in your code example?

Sorry if I'm not understanding your question here. I strongly suspected that that would be the behavior, so to verify I wrote exactly that code and observed the errors that occurred (I did change the text slightly to more specifically refer to the variable that the error was associated with). I admittedly relied on the IDE I was using rather than verifying through linting that that error did actually occur when compiled, but VS has been pretty reliable so far and running through the linter just now did provide the same errors 😄

image
image

As shown by the red on the right of that screenshot, this change makes a lot of tests unhappy.

I find it very counterintuitive. I would expect the find signature to restrict what types of functions are allowed to be passed, while the parameter list of the function itself (the arrow function in this case) determines the actual type of the function, and the site of invocation of the function (i.e., somewhere buried in the function body of find) to be the locus where TS would check whether the caller is passing the right types of arguments. The latter check is never applied in this case because Underscore itself is plain JS.

There's some additional explanation around this in Do's and Don'ts that might be helpful here.

The find signature does restrict what types of functions are allowed to be passed, but it also informs those functions that they can expect values for all three of the parameters. If Underscore were written in TS, the callback signature would also require the caller to provide all three parameters and error out if they didn't, while marking them as optional would allow Underscore to not supply them in some cases but also require the caller to check whether they were supplied, which I think makes sense.

With regards to the signature restricting the types of functions, as noted in that Do's and Don'ts documentation one can always write a callback that accepts fewer arguments than the caller will supply without marking the parameters themselves as optional since in JS a function can always be given more arguments than it actually has parameters for without any errors occurring (and before rest parameters at least that was often common practice for things that needed to accept a theoretically infinite number of parameters).

As for matcher et al returning a CollectionIterator with three required parameters: that wouldn't just be slightly off. It would break the code of anyone who invokes a matcher with only one argument.

Yup, that is definitely the case. That being said, if matcher et al were ever updated to need all three parameters, then not requiring them would break the code of anyone who chooses not to invoke them with all three, so if you were interested in future-proofing for the potential future you had hinted at here I think that would be what you would want to do. I agree that it's not advisable though, and it's not the course that I would choose myself.

@jgonggrijp
Copy link
Copy Markdown
Contributor

Thanks, it’s landing now. You are right that making the last two parameters of CollectionIterator optional is problematic.

I need to think longer about this, so perhaps I should just let it rest until it manifests in an actual review. I do need to mention one caveat in advance: not all functions are actually guaranteed to call the iteratee with three arguments. The counter-example that comes to mind is that _.sortedIndex will transform the query value through the iteratee.

@reubenrybnik
Copy link
Copy Markdown
Contributor Author

Good to know, thanks! I should probably include a fix for sortedIndex in cleanup work.

@reubenrybnik
Copy link
Copy Markdown
Contributor Author

reubenrybnik commented Aug 6, 2020

We did it @jgonggrijp! The full set of PRs minus cleanup are below.

Introduction of testing strategy (Round 1)
underscore-tests-chain-chunk - #45304
underscore-tests-chain-type-extractors - #45717

Introduction of commonly used types (Round 2)
underscore-tests-map-pluck-filter-groupBy-flatten - #45763

Collections (Round 3a)
underscore-tests-reduce-reduceRight - #45893
underscore-tests-find-reject - #45994
underscore-tests-where-findWhere-shuffle-sample - #46035
underscore-tests-each-partition-toArray - #46120
underscore-tests-every-some-contains - #46185
underscore-tests-min-max-size - #46240
underscore-tests-sortBy-indexBy-countBy-invoke - #46352

Arrays (Round 3b)
underscore-tests-first-initial-last-rest - #46184
underscore-tests-without-uniq-sortedIndex - #46068
underscore-tests-union-intersection-difference - #46419
underscore-tests-zip-unzip-object - #46306
underscore-tests-compact-range - #46189
underscore-tests-indexOf-lastIndexOf-findIndex-findLastIndex - #46585

Select Objects Functions (Round 3c)
underscore-tests-mapObject-pairs-findKey - #46422

The items I have planned for cleanup are as follows, where each main bullet point will be a PR:

  • Iteratees
    • Replace _ChainIteratee with Iteratee
    • Fix sortedIndex iteratee type
    • Update iteratee test group comments to use Underscore terms (e.g. partial object iteratee->matcher iteratee)
  • Comments + combinatorial test cleanup
    • Update summary comments to be 80 characters or less
    • Reorganize test variables as appropriate
    • Shorten test variable names where feasible
    • Update all test variables to be declare const
  • Non-combinatorial test cleanup
    • Use $ExpectType in more existing tests
    • Drop existing tests that seem redundant
    • Add more multi-call chain tests

Let me know if you'd like to see anything added or moved around in that list, and thanks in advance for continuing this journey with me a little further!

@jgonggrijp
Copy link
Copy Markdown
Contributor

If I'm right, this is the plan we agreed about earlier, so there is nothing more I want to add. I look forward to the last bits!

@reubenrybnik
Copy link
Copy Markdown
Contributor Author

There are admittedly a few items I mentioned over the course of these PRs that I've become a little meh on:

  • Replacing V and T with more descriptive parameter names: it would probably be nice to do that at some point, but I feel like it's not a huge detriment at the moment and it might be a better thing to do as part of possibly swapping T and V in the future.
  • Updating iteratees with boolean results to any: the definitions were already using boolean, that type is not as ambiguous as sorting types, requiring the use of a Boolean wrapper isn't a huge imposition, and it does seem like it might be a little nice to provide a hint to those newer at JS that thinking about the truthiness and falsiness of values is important.

@reubenrybnik
Copy link
Copy Markdown
Contributor Author

Still working on the last PR, progress so far can be observed in this diff: master...reubenrybnik:underscore-tests-usage-tests-cleanup

@reubenrybnik
Copy link
Copy Markdown
Contributor Author

Sorry this has taken a while. I've made all the changes I want to make in the last PR at this point, but I'm running into an issue with the linter that I can't seem to hack my way around, so I may need to put together an issue in https://github.com/Microsoft/dtslint for further investigation.

@reubenrybnik
Copy link
Copy Markdown
Contributor Author

reubenrybnik commented Sep 13, 2020

A nuance that escaped me here is that it seems like different versions of TS are expecting different results, which is why updating the $ExpectType declarations to match the original failures causes different failures - the run gets to a higher version encounters errors on a different version of TS, which evaluates results differently. I think the easiest way to work around this and to make it less likely that future folks will get stymied by this is to replace all usages of EnumerableKey with string | number. Possibly at some point in the future when microsoft/dtslint#57 is fixed that alias can be safely reintroduced.

Initial errors:

ERROR: 930:9   expect  TypeScript@4.1 expected type to be:
  string | number
got:
  EnumerableKey
ERROR: 980:5   expect  TypeScript@4.1 expected type to be:
  (string | number)[]
got:
  EnumerableKey[]
ERROR: 983:5   expect  TypeScript@4.1 expected type to be:
  (string | number)[]
got:
  EnumerableKey[]
ERROR: 986:5   expect  TypeScript@4.1 expected type to be:
  (string | number)[]
got:
  EnumerableKey[]
ERROR: 989:5   expect  TypeScript@4.1 expected type to be:
  (string | number)[]
got:
  EnumerableKey[]
ERROR: 993:9   expect  TypeScript@4.1 expected type to be:
  (string | number)[]
got:
  EnumerableKey[]
ERROR: 2813:5  expect  TypeScript@4.1 expected type to be:
  Dictionary<string | number>
got:
  Dictionary<EnumerableKey>
ERROR: 2814:5  expect  TypeScript@4.1 expected type to be:
  Dictionary<string | number>
got:
  Dictionary<EnumerableKey>
ERROR: 2815:5  expect  TypeScript@4.1 expected type to be:
  ChainType<Dictionary<string | number>, string | number>
got:
  ChainType<Dictionary<EnumerableKey>, EnumerableKey>

Errors when $ExpectType declarations are updated to use EnumerableKey:

Error: C:/Repos/DefinitelyTyped/types/underscore/underscore-tests.ts:930:9
ERROR: 930:9   expect  TypeScript@3.7 expected type to be:
  EnumerableKey
got:
  string | number
ERROR: 980:5   expect  TypeScript@3.7 expected type to be:
  EnumerableKey[]
got:
  (string | number)[]
ERROR: 983:5   expect  TypeScript@3.7 expected type to be:
  EnumerableKey[]
got:
  (string | number)[]
ERROR: 986:5   expect  TypeScript@3.7 expected type to be:
  EnumerableKey[]
got:
  (string | number)[]
ERROR: 989:5   expect  TypeScript@3.7 expected type to be:
  EnumerableKey[]
got:
  (string | number)[]
ERROR: 993:9   expect  TypeScript@3.7 expected type to be:
  EnumerableKey[]
got:
  (string | number)[]
ERROR: 2813:5  expect  TypeScript@3.7 expected type to be:
  Dictionary<EnumerableKey>
got:
  Dictionary<string | number>
ERROR: 2814:5  expect  TypeScript@3.7 expected type to be:
  Dictionary<EnumerableKey>
got:
  Dictionary<string | number>
ERROR: 2815:5  expect  TypeScript@3.7 expected type to be:
  ChainType<Dictionary<EnumerableKey>, EnumerableKey>
got:
  ChainType<Dictionary<string | number>, string | number>

danielrearden pushed a commit to danielrearden/DefinitelyTyped that referenced this pull request Sep 22, 2020
…rray Tests - Map, Pluck, Filter, GroupBy, and Flatten by @reubenrybnik

* Adding type extractors and updating existing tests to use them.

* Improving handling of mixed-iterability types by simplifying the Underscore and chain functions to a single overload.

* Updating TypeOfCollection to check against List and Dictionary separately instead of using Collection since using the latter behaves oddly with JQuery objects.

* Renaming a variable to something slightly more friendly.

* Updating type definitions for map, pluck, filter, groupBy, and flatten and removing _ChainOfArrays and IterateeMatcherShorthand.

* Adding tests for map, pluck, filter, groupBy, and flatten.

* Fixing a test for @types/parse that breaks with these changes.

* Updating more places to use returns instead of return because I want to.

* Revising identifiers in tests.

* Switch from $ExpectType to assignability checks for now due to an odd issue with the linter that seems to affect TS 3.0 specifically.

* Updating a word in a comment that I forgot to update.

* Revert "Fixing a test for @types/parse that breaks with these changes."

This reverts commit 1955fc1.

* Fixing a typo in a variable name.

* Dropping KeysOfUnion and TypesOfUnionProperty for now in favor of a safer and simpler keyof T + string approach.

* Reducing the dimensions of a deep flatten and changing the names of the resulting types.

* Dropping array tests for most functions since testing lists should be sufficient to also cover arrays.

* Adding a comment to every test group.

* Update functions that take an iteratee to use an Iteratee type and updating map to use an IterateeResult type.

* Shortening the names of nested list variables.

* Also collapsing pluck to a single overload.

* Switching groupBy back to allowing any rather than attempting to constrain to typical valid key types.

* Dropping unnecessary `keyof T` constraints and updating `string` and `string | number` constraints to `PropertyKey` since from experimentation Underscore works just fine with symbols as well.

* Wrapping return values for multi-line funciton declarations to their own line.

* Updating UnderscoreStatic functions that don't technically need the collection type to use it anyway for consistency with those that do.

* Renaming some mixed-dimension array variables one more time.

* Adding property path tests for groupBy.

* Changing "iterator" to "iteratee" in test group comments.

* Adding property path tests for filter.

* Updating IterateeResult to only care about the return type of an iteratee function since that's really all that matters from its perspective to fix issues with using function iteratees on values of type any.

* Working around some _Chain return types incorrectly using non-collections as their wrapped values.

* Using `T` rather than `TypeOfCollection<V>` in `Underscore.map` since I have it.

* Applying a shorter variable name that I had missed copying over from DefinitelyTyped#45201.

* Fixing consistency issues with two more test variable names.

* Adding EnumerableKey and using that intstead of PropertyKey.
…ay Tests - Usage Tests Cleanup by @reubenrybnik

(cherry picked from commit 61deea7)
@reubenrybnik
Copy link
Copy Markdown
Contributor Author

reubenrybnik commented Sep 25, 2020

The final set of PRs for this project are as follows:

Introduction of testing strategy (Round 1)
underscore-tests-chain-chunk - #45304
underscore-tests-chain-type-extractors - #45717

Introduction of commonly used types (Round 2)
underscore-tests-map-pluck-filter-groupBy-flatten - #45763

Collections (Round 3a)
underscore-tests-reduce-reduceRight - #45893
underscore-tests-find-reject - #45994
underscore-tests-where-findWhere-shuffle-sample - #46035
underscore-tests-each-partition-toArray - #46120
underscore-tests-every-some-contains - #46185
underscore-tests-min-max-size - #46240
underscore-tests-sortBy-indexBy-countBy-invoke - #46352

Arrays (Round 3b)
underscore-tests-first-initial-last-rest - #46184
underscore-tests-without-uniq-sortedIndex - #46068
underscore-tests-union-intersection-difference - #46419
underscore-tests-zip-unzip-object - #46306
underscore-tests-compact-range - #46189
underscore-tests-indexOf-lastIndexOf-findIndex-findLastIndex - #46585

Select Objects Functions (Round 3c)
underscore-tests-mapObject-pairs-findKey - #46422

Cleanup
underscore-tests-iteratee-cleanup - #46634
underscore-tests-shortening-cleanup - #46799
underscore-tests-usage-tests-cleanup - #47543

The final diff for this PR contains changes from a few additional PRs (#45894, #46650, #47539) that were not technically part of this project but took place in this timeframe and worked toward the same ends as well as a contribution from @ProdigySim (#45842). Tons of thanks to @jgonggrijp, who was a tireless reviewer who made major contributions to the success of this project.

Additional closing remarks for this project can be found in #47543.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Perf: Worse typescript-bot determined that this PR has a negative impact on compilation performance. Popular package This PR affects a popular package (as counted by NPM download counts). Where is GH Actions? GH Actions didn't give a response to this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants