8000 [lodash] Better chaining wrappers, overload clean-up by aj-r · Pull Request #19356 · DefinitelyTyped/DefinitelyTyped · GitHub
[go: up one dir, main page]

Skip to content

[lodash] Better chaining wrappers, overload clean-up #19356

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

Merged
28 commits merged into from
Oct 19, 2017

Conversation

aj-r
Copy link
Contributor
@aj-r aj-r commented Aug 26, 2017

Fixes #14337, Fixes #19228, Fixes #14748, Fixes #18956, Fixes #14282

Apologies for the massive PR. The fix for #14337 required me to modify the chaining wrappers for every single function, so I figured I should clean up some 8000 of the messy overloads along the way (which also fixed several other issues).

Unfortunately, this will be a breaking change in terms of what type arguments are accepted. So if anyone was explicitly passing type arguments (e.g. _.chain([]).find<_.LoDashExplicitObjectWrapper<any>>('a')), their code may be broken. But I think in the long run this change should be more helpful than harmful, as it removes the need to explicitly pass type arguments in almost all cases.

Summary of changes:

  • The chain wrapper interfaces have been greatly simplified. Instead of many wrapper types (LoDashImplicitArrayWrapper, LoDashImplicitObjectWrapper, LoDashImplicitNumberArrayWrapper, etc.), there are now only 2 wrapper types: LoDashImplicitWrapper and LoDashExplicitWrapper.
    • This allows the return types to be more accurate (especially for functions like head and find).
  • Cleaned up messy function overloads (removed unnecessary type arguments)
    • Made iteratee arguments more consistent - now they all use one of the new Iteratee types, unless they have a good reason not to.
  • Replaced Object and Function types with more appropriate types
  • Changed many Dictionary<T> inputs to just take plain objects, so an index signature is not required

Please fill out this template:

  • 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.)
  • Follow the advice from the readme.
  • Avoid common mistakes.
  • Run npm run lint package-name (or tsc if no tslint.json is present).

Select one of these and delete the others:

If changing an existing definition:

  • Provide a URL to documentation or source code which provides context for the suggested changes: https://lodash.com/docs/4.17.4
  • Increase the version number in the header if appropriate. (not appropriate)
  • If you are making substantial changes, consider adding a tslint.json containing { "extends": "dtslint/dt.json" }.

@dt-bot
Copy link
Member
dt-bot commented Aug 26, 2017

types/lodash/index.d.ts

to authors (@bczengel @chrootsu @stepancar @ericanderson @Ailrun). Could you review this PR?
👍 or 👎?

Checklist

  • pass the Travis CI test?

@typescript-bot typescript-bot added The Travis CI build failed Revision needed This PR needs code changes before it can be merged. labels Aug 26, 2017
@typescript-bot
Copy link
Contributor
typescript-bot commented Aug 26, 2017

@aj-r Please fix the failures indicated in the Travis CI log.

@aj-r
Copy link
Contributor Author
aj-r commented Aug 26, 2017

Checks are now passing @bczengel @chrootsu @stepancar @ericanderson @Ailrun

@aj-r aj-r force-pushed the better-wrappers branch 2 times, most recently from acd53ee to 48d2b05 Compare August 30, 2017 15:39
@aj-r aj-r force-pushed the better-wrappers branch from 48d2b05 to 2a19046 Compare August 30, 2017 15:53
@ghost
Copy link
ghost commented Oct 19, 2017

Thanks!

@ghost ghost merged commit 77eac54 into DefinitelyTyped:master Oct 19, 2017
@Thorarin
Copy link

Ouch, why were the generic type parameters for defaultsDeep removed?

@thorn0
Copy link
Contributor
thorn0 commented Oct 19, 2017

@Thorarin

If a type parameter does not appear in the types of any parameters, you don't really have a generic function, you just have a disguised type assertion.

-- README, Common mistakes

@alan-agius4
Copy link
Contributor

After this change I am getting ‘Index signature is missing’ when i do _.flatMap({a:1 b:2});

Is this intended?

@aj-r
Copy link
Contributor Author
aj-r commented Oct 20, 2017

@alan-agius4 No, that was not indended. However, this code works for me in TS 2.4 and 2.2. What version are you using, and can you provide more context?

const res: number[] = _.flatMap({ a: 1, b: 2 });

@alan-agius4
Copy link
Contributor

@aj-r I am using TypeScript: 2.5.5

interface Foo {
	bar: number;
	foo: number;
}

const x: Foo = {
	bar: 1,
	foo: 1
};

const y = _.flatMap(x); // this is an error

@dawnmist
Copy link
Contributor

I am having issues with the definition for omit, typescript version 2.5.3

Using redux, we have a reducer state definition of:

export type Activity = ActivityDataType & {
  Ref: RecordReferenceNumber;
  RefKey: string;
};
export interface ActivitiesByRef {
  [refKey: string]: Activity;
};

Trying to use omit to remove 'deleted' keys in the reducer now gets rejected by the compiler. For the deletion action the reducer returns:

activitiesById = (state: ActivitiesByRef = {}, action: Action): ActivitiesByRef => {
  // process actions...
  if (action.type === 'DELETE_ACTIVITIES') {
    const deletedRefKeys: string[] = action.payload.deletedIds;
    return omit(state, deletedRefKeys);
  }
}

Result is now the compile error for the use of omit:

Type 'Partial' is not assignable to type 'ActivitiesByRef'.
Index signatures are incompatible.
Type 'Activity | undefined' is not assignable to type 'Activity'.
Type 'undefined' is not assignable to type 'Activity'.
Type 'undefined' is not assignable to type 'ActivityDataType'.

Given that the keys are variable anyway, the actual end result for omit in this case is actually still ActivitiesByRef rather than Partial<ActivitiesByRef>. It's just one with (potentially) a few less keys in the object. I can work around it by recasting the return value from omit to ActivitiesByRef again (so return omit(state, deletedRefKeys) as ActivitiesByRef;, but it represents a missed use case in the handling of the return definition. (Which may actually represent a Typescript bug, in that "Partial" for objects with variable keys really should be treated as being equal to those objects).

@thorn0
Copy link
Contributor
thorn0 commented Oct 21, 2017

Partial<{ [key: string]: SomeType; }> is effectively the same as { [key: string]: SomeType | undefined; }, hence the type incompatibility. See also re index signatures and strict null checks: microsoft/TypeScript#13778

For now we can solve this by adding an overload for omit:

        omit<T>(
            object: Dictionary<T>,
            ...paths: Many<string | number>[]
        ): Dictionary<T>;

@ericanderson
Copy link
Contributor

I’m terribly confused. Why aren’t we using

type Diff<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = {[P in Diff<keyof T, K>]: T[P]};

From microsoft/TypeScript#12215 (comment)

@thorn0
Copy link
Contributor
thorn0 commented Oct 21, 2017

At least because omit supports not only simple property names, but also paths like foo[0].bar.

@ericanderson
Copy link
Contributor

Ah. Nm. I didn’t read the above code sample.

@ericanderson
Copy link
Contributor

@dawnmist ActivitiesByRef is probably typed wrong. Your definition says that no matter what I pass in as the key I will get back an Activity. In reality, I presume, you may also get an undefined. If you update your index definition to reflect this your compiler error will go away.

@thorn0
Copy link
Contributor
thorn0 commented Oct 21, 2017

In general, everything associated with those deep property paths is really difficult to type in a correct and/or practical way. For instance, I tried to improve types for _.zipObjectDeep doing something like this:

declare function zipObjectDeep<T>(
  paths: Array<string | number | Array<string | number>>,
  values: T[]
): NestedDictionary<T>;

interface NestedDictionary<T> {
  [ key: string ]: T | NestedDictionary<T>;
}

But how would we use a value of the NestedDictionary type in practice?

const a = zipObjectDeep(['foo'], [1]); // number | NestedDictionary<number>

What can be done with a.foo? We still have to use a type assertion to do anything meaningful with it . Even type guards don't work with index signatures (microsoft/TypeScript#17567). If a type assertion is still needed how are these bells and whistles better than simple any or object?

Then I realized that T | NestedDictionary<T> isn't even the correct type for values because the paths can also be like foo[0], i.e. I forgot about arrays. Can we fix it?

interface NestedDictionary<T> {
  [ key: string ]: T | NestedDictionary<T> | Array<T | NestedDictionary<T>>;
}

Right? No, because arrays can contain other arrays (foo[0][0]), and it's something that TypeScript simply doesn't support (see microsoft/TypeScript#13431 (comment)).


The situation with _.omit is similar to that. Actually, Partial<T> (as well as Dictionary<T>) isn't the correct return type. To be 100% correct, we need to use PartialDeep, which even is already present in lodash.d.ts. But how practical will it be to use? It looks to me like a great way to get bogged down in type assertions and null checks. Some trade-offs have to be made.

@aj-r
Copy link
Contributor Author
aj-r commented Oct 21, 2017

@dawnmist I think the best solution in your case would be to change your ActivitiesByRef` definition to this:

export interface ActivitiesByRef {
  [refKey: string]: Activity | undefined;
};

Since it's impossible for every key in ActivitiesByRef to have a value, you should really be forcing your code to check that it's not undefined before using it.

That said, I think @thorn0 's overload makes a lot of sense, too, and we should add it, provided that it doesn't break any existing use cases:

        omit<T>(
            object: Dictionary<T>,
            ...paths: Many<string | number>[]
        ): Dictionary<T>;

@dawnmist
Copy link
Contributor
dawnmist commented Oct 21, 2017

@ericanderson If you pass in a key that exists in the object, you are guaranteed an Activity in return. It is only when the key itself doesn't exist in the object that you'd get undefined. And in that case, it is undefined because it is not there, not because it is stored as an undefined value. So the object type should be right - but access functions would have the undefined as a possible output for invalid keys.

 getActivity(state: ActivitiesByRef, id: string): Activity | undefined;

That is a standard Redux pattern - storing a list of items in an object with a unique key to identify each item so you can update/remove/add/retrieve items to the list via the key. At no time should a key that exists in the object point to anything except a list item (in this case, an Activity).

Otherwise, what is the point of a mapped type if the type can never be properly mapped? What you are saying is that a typescript declaration of

interface Example {
  [key: string]: number;
}

is never a valid definition - despite this being a typical example from the Typescript Handbook.
Edit: The handbook calls them "Indexable Types": https://www.typescriptlang.org/docs/handbook/interfaces.html

@thorn0
Copy link
Contributor
thorn0 commented Oct 21, 2017

@dawnmist @ericanderson That's discussed in microsoft/TypeScript#13778 and microsoft/TypeScript#11238 (comment). TL;DR: those who really want it should add | undefined to their index signatures themselves, but it might create more hassle than value.

@thorn0
Copy link
Contributor
thorn0 commented Oct 21, 2017

@aj-r I've added this overload to #20717, but there is something strange going on with Travis there.

@aj-r
Copy link
Contributor Author
aj-r commented Oct 21, 2017

@alan-agius4 For that particular use case, you should use _.values instead of _.flatMap. However, that does point me at a use case that I think you're getting at:

interface Foo {
	bar: number;
	foo: number[];
}

const x: Foo = {
	bar: 1,
	foo: [2, 3],
};

const y = _.flatMap(x); // should return number[]

Unfortunately, I can't find a good way to handle this that doesn't involve an index signature. The best I can do is this:

flatMap(
    collection: object | null | undefined
): any[];

I'll add this overload.

@aj-r aj-r deleted the better-wrappers branch October 21, 2017 14:14
@ericanderson
Copy link
Contributor

In my code bases, we set index values to be | undefined because code that access them need to do the check and people may forget otherwise.

I think here in the non-nested case, the code is actually helping prevent bugs. The returned type is no longer valid even your type says “all keys have a defined value”.

I’m the case where you might Object.keys, you can always add the exclamation point.

There has long been a discussion about undefined as a value and undefined as in there is no key. This has come up a lot with things like Partial as optionality implies | undefined.

@aj-r
Copy link
Contributor Author
aj-r commented Oct 21, 2017

@ericanderson I'm a fan of putting | undefined on index signatures, too. But at the same time, I don't think we should enforce that style on everyone who uses lodash, especially if it is still up for debate. I think it's best for our code to be compatible with both styles if possible.

@aj-r aj-r mentioned this pull request Oct 21, 2017
8 tasks
@alex-kinokon
Copy link
Contributor

By the way, this pull request breaks either _.isFunction (line 9619) or TypeScript, because the value will not be recognized as a callable expression.

image

@thorn0
Copy link
Contributor
thorn0 commented Oct 23, 2017

| Function should be removed from there. Function, Object, Number and so on are interfaces needed only to define methods of the corresponding prototypes. These types shouldn't be used for anything else.

@aj-r
Copy link
Contributor Author
aj-r commented Oct 23, 2017

Agreed. I'll fix this in #20795.

This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Popular package This PR affects a popular package (as counted by NPM download counts).
Projects
None yet
0