lodash / lodash

A modern JavaScript utility library delivering modularity, performance, & extras.
https://lodash.com/
Other
59.75k stars 7.02k forks source link

.filter() typing breaks on unions of arrays #5928

Open Roanmh opened 2 weeks ago

Roanmh commented 2 weeks ago

The following code errors for lodash but not for the similar implementation using the built in .filter(). I encountered this, but my minimal repro is inspired by @ialexryan 's report of microsoft/TypeScript#44373. This is also true for find, every, some, etc

import _ from "lodash"

interface Fizz {
    id: number;
    fizz: string;
}

interface Buzz {
    id: number;
    buzz: string;
}

([] as Fizz[] | Buzz[]).filter(item => item.id < 5); // ok

_.filter([] as Fizz[] | Buzz[]).filter(item => item.id < 5);
//                                                  ^^ error
Property 'id' does not exist on type 'number | Fizz | Buzz | { <S extends Fizz>(predicate: (value: Fizz, index: number, array: Fizz[]) => value is S, thisArg?: any): S[]; (predicate: (value: Fizz, index: number, array: Fizz[]) => unknown, thisArg?: any): Fizz[]; } | ... 60 more ... | ((searchElement: Buzz, fromIndex?: number | undefined) => boolean)'.
  Property 'id' does not exist on type 'number'.

or view on TS Playground

Note: Typescript would error as well until this was fixed by Typescript 5.2.2

ruchivora commented 1 week ago

When you use the native Array.prototype.filter() function on a union type (like Fizz[] | Buzz[]), TypeScript is more lenient and tries to infer the type based on the structural typing principle. Structural typing means that TypeScript checks if a type has the necessary properties at runtime, rather than enforcing strict type rules upfront.

TypeScript assumes that both Fizz and Buzz might have an id property, and it allows you to proceed without throwing an error. This leniency can lead to runtime errors if one of the types (e.g., Buzz) doesn't actually have the id property.

Lodash’s TypeScript type definitions are stricter because they want to ensure you don’t encounter runtime issues by accidentally accessing properties that don't exist on certain types. So you can use something like:

interface Common { id: number; }

interface Fizz extends Common { // Other properties for Fizz }

interface Buzz extends Common { // Other properties for Buzz }

_.filter([] as Fizz[] | Buzz[]).filter(item => item.id < 5);

Now _filter can be sure that both Fizz and Buzz has a id property.