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

Lodash's mapValues and empty object type interact confusingly #29901

Closed
seansfkelley opened this issue Feb 13, 2019 · 6 comments
Closed

Lodash's mapValues and empty object type interact confusingly #29901

seansfkelley opened this issue Feb 13, 2019 · 6 comments
Assignees
Labels
Needs Investigation This issue needs a team member to investigate its status.

Comments

@seansfkelley
Copy link

seansfkelley commented Feb 13, 2019

I had trouble coming up with a good name and search terms for this issue. It looks like it's triggering some heuristic that is misbehaving and is very strange. Please feel free to rename if you can come up with a better summary for it.

TypeScript Version: 3.3.1

Search Terms: mapvalues ternary side effect unused inference empty object type

Code

// Begin types from Lodash.
interface Dictionary<T> {
  [index: string]: T;
}

interface NumericDictionary<T> {
  [index: number]: T;
}

type ObjectIterator<TObject, TResult> = (value: TObject[keyof TObject], key: string, collection: TObject) => TResult;

type DictionaryIterator<T, TResult> = ObjectIterator<Dictionary<T>, TResult>;

// In lodash.d.ts this function has many overloads, but this seems to be the problematic one.
function mapValues<T, TResult>(obj: Dictionary<T> | NumericDictionary<T> | null | undefined, callback: DictionaryIterator<T, TResult>): Dictionary<TResult> {
  return null as any;
} 
// End types from Lodash.

interface Foo {
    foo: string;
} 

interface Bar {
    bar: string;
}

export function fooToBar(
  foos: Record<string, Foo>,
): Record<string, Bar | null> {
  // The next line, despite being unrelated to the other two lines, causes them to fail.
  // Comment it out to see the function compile erroneously.
  //
  // This behavior only happens when `result` is used as an intermediate: if you return 
  // the ternary directly, the function always compiles.
  //
  // I couldn't find another Lodash function for which this issue reproduced after sampling
  // a few. 
  const wat = mapValues(foos, f => f.foo);
  const result = foos == null ? {} : mapValues(foos, f => f.foo);
  // This line _should_ fail, because `result` is not the right type.
  return result;
}

Expected behavior: fails to compile without the const wat line present.

Actual behavior: only fails to compile when const wat is present.

Playground Link: link

Related Issues:

@jack-williams
Copy link
Collaborator

I think this might caused by the same issue in #29393.

@seansfkelley
Copy link
Author

@jack-williams that seems likely, but I'll keep the issue open and leave it up to the maintainers to judge if that is the case.

@weswigham
Copy link
Member

Oh boy. So this strangeness really is strange. All that differs from the wat line existing and not is the type ids we assign to the object type and the Dictionary instantiation. With wat, Dictionary<string> gets a lower type id, which makes the result union Dictionary<string> | {}, which when subtype reduces (correctly) to {}. When the wat line is gone, the ordering of the assigned type ids inverts, making the union we make {} | Dictionary<string>, which we then seem to say that subtype reduces to Dictionary<string> (which is clearly incorrect, {} should not be considered a subtype of a Dictionary<anything>, since a Dictionary has an index signature).

@jack-williams
Copy link
Collaborator

Ahh the type ids. An amateur mistake by me there to overlook that one, haha.

@weswigham
Copy link
Member

It's probably also worth noting that I believe by our normal subtype reduction rules, result's type should always simplify to type {}, which is assignable to the return type, despite not having an index signature, and despite having something with potentially incompatible member types assigned to it - which is a manifestation of the aliasing problem we show elsewhere. :(

@seansfkelley
Copy link
Author

Hah, what a bug. Thanks for the explanation @weswigham!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

4 participants