Repository navigation
Add BooleanConstructor as an overload to .filter to allow for easy type predicate filtering #50387
Description
Activity
Related: #16655 “Boolean() cannot be used to perform a null check”
A bit convoluted to understand for most people, isn't it?
It looks more like a JS quirks that TS was supposed to prevent than beautiful TS codeReacted by Benjamin Michotte, Matthew Francis Brunetti, Thundercraft5, Tariq Khan, sillysillyc, Andy Hails, Conor Rice and Ryan Christiansee #50377
Grégory Houllier (@ghoullier) Aha, didn't see that Orta Therox (@orta) had already tried it.
LMK how I can help.
A bit convoluted to understand for most people, isn't it? It looks more like a JS quirks that TS was supposed to prevent than beautiful TS code
Louis Frament (@lf-novelt) Could you clarify what you mean? Which bits are convoluted?
The point of intermediate-advanced TS is to try to make the beginner's TS life easier while staying out of the way. I'd argue that this overload does that very well.
.filter(Boolean)is a common pattern at all levels of JS, and using it to make TS's inference smarter benefits everyone.Reacted by Dan Wood, Jelte Homminga, Levi Buzolic, Nicu Chiciuc, Emilio Martinez, jf-gofore, Brett Bloxom, Andy Hails, Conor Rice and Josh Ghoulberg 👻Indeed this syntax is a cool trick but not obvious to understand at first glance. I don't mind TS supporting it but would not push for this example as a "go to" to filter nulls or undefined from an array.
As some wise man said, you only write code once, but read it many times, so I would write code that's easy to read even if it costs me a few more keystrokes.And
arr.filter(i => i)is even shorter.But I still think
arr.filter(i => i !== undefined && i !== null)is more explicit (and it does not filter out empty string, which is clear)Reacted by Craig P HicksMartinJohns commented
on Aug 21, 2022 ContributorMore actionsDuplicate of #16655.
Martin Johns (@MartinJohns) I don't think it is, it's a separate suggestion which is of much smaller impact.
In fact, you could consider this issue just a moving of this great suggestion into its own issue.
MartinJohns commented
on Aug 21, 2022 ContributorMore actionsTrue, it's only a partial duplicate, but your use case is the main one mentioned in #16655 and most of its duplicates.
But I said my bit and I'll drop out of this conversation. Considering the unnatural amount of upvotes this issue already has it's clear that it has been promoted somewhere else.
But just FYI, this suggestion is already being tried out: #50377
Reacted by Matt Pocock, Toni Villena, Tristan Hessell , Andrii Dieiev and Thundercraft5- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptIn DiscussionNot yet reached consensusNot yet reached consensus
on Sep 27, 2022 This fails and it shouldn't.
const a = [] as InputsDataType[] | string[] | AppButtonType[] | CustomButtonT[]; const testFilter = (data: InputsDataType): data is InputsDataType => { return true; } a.filter(testFilter);
After #57465 there's less need for this. You can write
arr.filter(x => !!x)instead ofarr.filter(Boolean)and it will work for object types, where there are no footguns around 0 and "".That being said, there was a lot of confusion on Twitter about whether
filter(Boolean)will now work. The answer is no. I spent some time trying to fix this in a few different ways. I wasn't able to, but I learned enough along the way that I thought it would be useful to dump state.Approach 1: add an overload for filter(BooleanConstructor) aka the ts-reset approach
Here's one version of this that specializes on
thisto restrict to arrays of objects:diff --git a/src/lib/es5.d.ts b/src/lib/es5.d.ts index e404df509a..3bbf59bbba 100644 --- a/src/lib/es5.d.ts +++ b/src/lib/es5.d.ts @@ -1252,6 +1252,12 @@ interface ReadonlyArray<T> { * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. */ map<U>(callbackfn: (value: T, index: number, array: readonly T[]) => U, thisArg?: any): U[]; + filter(this: ReadonlyArray<object | null | undefined>, predicate: BooleanConstructor, thisArg?: any): (T & {})[]; /** * Returns the elements of an array that meet the condition specified in a callback function. * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array. @@ -1443,6 +1449,12 @@ interface Array<T> { * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. */ map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; + filter(this: Array<object | null | undefined>, predicate: BooleanConstructor, thisArg?: any): (T & {})[]; /** * Returns the elements of an array that meet the condition specified in a callback function. * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
Here's another that uses a conditional type to similar ends:
diff --git a/src/lib/es5.d.ts b/src/lib/es5.d.ts index e404df509a..eaabc04b1b 100644 --- a/src/lib/es5.d.ts +++ b/src/lib/es5.d.ts @@ -1258,6 +1258,12 @@ interface ReadonlyArray<T> { * @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value. */ filter<S extends T>(predicate: (value: T, index: number, array: readonly T[]) => value is S, thisArg?: any): S[]; + filter(predicate: BooleanConstructor, thisArg?: any): [T] extends ([object|null|undefined]) ? (T & {})[] : T[]; /** * Returns the elements of an array that meet the condition specified in a callback function. * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array. @@ -1449,6 +1455,12 @@ interface Array<T> { * @param thisArg An object to which the this keyword can refer in the predicate function. If thisArg is omitted, undefined is used as the this value. */ filter<S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[]; + filter(predicate: BooleanConstructor, thisArg?: any): [T] extends ([object|null|undefined]) ? (T & {})[] : T[]; /** * Returns the elements of an array that meet the condition specified in a callback function. * @param predicate A function that accepts up to three arguments. The filter method calls the predicate function one time for each element in the array.
The problem with both of these is that they fundamentally clash with the fallback logic that was introduced for
(A[] | B[]).filterin #53489. When you call a method on(A[] | B[])and TS can't find any candidate overloads, it takes a look at(A | B)[]instead. Once you introduce theseBooleanConstructoroverloads, it does find a candidate overload. So the fallback logic doesn't run and you get cryptic errors for calls like this:([] as Fizz[] | Buzz[]).filter(item => item.id < 5); // ~~~~~~~~~~~~~~~~~~~ // Argument of type '(item: any) => boolean' is not assignable to parameter of type 'BooleanConstructor'.
I don't see how you'd fix this issue. Since ts-reset uses a similar approach, it also breaks this sort of call: mattpocock/ts-reset#168
Approach 2: Make BooleanConstructor a type predicate
We can change the call signature of
BooleanConstructorto be a type predicate for object types:diff --git a/src/lib/es5.d.ts b/src/lib/es5.d.ts index e404df509a..f8e8df6522 100644 --- a/src/lib/es5.d.ts +++ b/src/lib/es5.d.ts @@ -532,7 +532,8 @@ interface Boolean { interface BooleanConstructor { new (value?: any): Boolean; - <T>(value?: T): boolean; + <T>(value?: T): value is ([T] extends [object | null | undefined] ? T & {} : T); readonly prototype: Boolean; }
This is conceptually simpler but it also runs into a few problems.
First, TS actually made this change back in 2019 with #29955. But it was quickly reverted in #31515 because it had an undesirable effect on
any[].filter(Boolean).Second, it doesn't work! Even with this change, TS doesn't select the right overload of
Array.prototype.filter. You can see this independently of the built-inBooleanConstructor:declare let dates: (Date|null)[]; interface BCWithNew { // Comment out the next line to get "nonNullDates: Date[]" below new (value?: any): Boolean; <T>(value?: T): value is T & {}; readonly prototype: Boolean; } declare let bcN: BCWithNew; const nonNullDates = dates.filter(bcN); // ^? const nonNullDates: (Date | null)[] type T = BCWithNew extends (value: Date | null, index: number, array: (Date|null)[]) => value is Date ? true : false; // ^? type T = true
I think this is a TS bug. Mateusz Burzyński (@Andarist) dug into why this happens, see this thread. My understanding is that it is a correctness / expedience tradeoff in finding the right overload.
The issue around
any[].filter(Boolean)returningunknown[]is fixed by #56373. I'm not sure what it would take make TypeScript pick the correct overload ofArray.filter.So in conclusion, it's harder to improve
filter(Boolean)than I expected. You may as well usefilter(x => !!x)instead.Reacted by Sid Vishnoi, Toni Villena, Max Barvian, Andrew Kazakov, Justin H., Andreas Bergmaier, Paweł Dymara and austinw-fineartReacted by Toni Villena and Vyacheslav Popovshadow-identity commented
on May 24, 2024 More actionsDespite
.filter()is now somewhat fixed in TypeScript 5.5,myArray.filter(Boolean)type still fails to be calculated properly. I hope that the sort of attention to TS 5.5 release can help to solve this issue.Reacted by Charlie Harding, Cameron Steffen, Paweł Dymara, Maikel van Dort and Kevin DengDespite
.filter()is now somewhat fixed in TypeScript 5.5,myArray.filter(Boolean)type still fails to be calculated properly. I hope that the sort of attention to TS 5.5 release can help to solve this issue.Another use case with boolean, something like;
// Cast to any to simulate incoming API value const array: string[] | undefined | null = ['aa'] as any; const isValid = Boolean(array && array.length > 0); // Error on `array.map` for possible null or undefined, while we already verified the existence & length before if (isValid) array.map(e => console.log(isValid));
It ignores the fact the existence & length was already checked before
For what it's worth, a number of people in this thread are recommending to do
.filter(x => !!x)instead but that doesn't actually narrow it either.const arr = ['dog', 'cat', null]; arr.filter(item => typeof item === 'string').map(item => item.toUpperCase()); arr.filter(Boolean).map(item => item.toUpperCase()); // 'item' is possibly 'null'.(18047) arr.filter(x => !!x).map(item => item.toUpperCase()); // 'item' is possibly 'null'.(18047)
lib Update Request
Configuration Check
My compilation target is
ES2015and my lib isthe default.Missing / Incorrect Definition
I would love to add an overload to Array.filter which takes in a common use case of passing a
Booleandirectly to.filter. This would offer a simple, intuitive workaround for a common pain point with.filter.This could be achieved via an overload adding
BooleanConstructor, and a type predicate.https://www.typescriptlang.org/play?#code/PTAEHUEsBcAtTgU1AewG6IE4BsUEMATAWACgBjFAOwGdpQ9NNQBeUAbQCIBGDgGlACulAogBmkSogIBdANykQoJaAB6AflKkJ0LKLxlkAQUZ4AngB4AKgD5QAb1JLx2HZgAUAB0xTIZPDoAuUAAhFBRsRDxKAGEqWkwBMmgUTABKIIA5KgAxPGxqCxs2OVIAX00SaFMPZCzKXPzC21ZLUEQADx1halAABlAAH1AAcmHB0EoBbGxxoRFxSQJQNQnEDCYgy3kSUgoaOmdXKWMmVgZMADpDrDdQ8MjKVO3FZXUgA
Sample Code
filteredArrshould bestring[], not(string | undefined)[].