-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Infer type guard => array.filter(x => !!x) should refine Array<T|null> to Array<T> #16069
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Comments
For filter specifically, #7657 would be something we can do here. but that still requires you to write an explicit type guard annotation somewhere, |
Reopening to track this since both other issues now target lib.d.ts changes |
Subscribing since this is a common pattern I use after a map |
Just chiming in to say that this shouldn't be fixed just for Arrays. |
Here's a workaround:
Instead, you can do this:
I believe that the first snippet doesn't work due to TS not being able to automatically infer that the callback function inside the filter is a type guard. So by explicitly defining the function's return type as ################################################### That being said, I hope that we can get this type guard inference into the language. :) |
As I said in #10734 (comment), I'm very eager to see type guards inferred and have already implemented it prototypically for a subset of arrow expressions. If you get around to specifying the workings of type guard inference (i.e. the hard part), I'd gladly get involved in the implementation (the easier part). |
@dgreene1 I trid to write a simple operator to make the guard simpler to use, but failed. Do you have any suggestion? import { Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
export function isNotNullOrUndefined<T>(input: null | undefined | T): input is T {
return input != null;
}
export function skipEmpty<T>() {
return function skipEmptyOperator(source$: Observable<T>) {
return source$.pipe(filter(isNotNullOrUndefined));
};
} @Andy-MS @mhegazy could you help to improve the types above? |
@scott-ho https://twitter.com/martin_hotell/status/999707821980647424 |
@scott-ho, I'd check out the approach @Hotell shared. I wish I could help you more, but I'm not familiar with rxJs yet. So I'm not really sure what |
@Hotell Thanks for your guidance. It seems your solution only works in v2.8 or above. And I finally make it works import { Observable } from 'rxjs';
import { filter } from 'rxjs/operators';
export function isNotNullOrUndefined<T>(input: null | undefined | T): input is T {
return input != null;
}
export function skipEmpty<T>() {
return function skipEmptyOperator(source$: Observable<null | undefined | T>) {
return source$.pipe(filter(isNotNullOrUndefined));
};
} |
I thought I'll share my solution export type Empty = null | undefined;
export function isEmpty(value: any): value is Empty {
return [null, undefined].includes(value);
}
export function removeEmptyElementsFromArray<T>(array: Array<T | Empty>): T[] {
return array.filter((value) => !isEmpty(value)) as T[];
} example: const nums = [2, 3, 4, null] // type is (number | null)[]
const numsWithoutEmptyValues = removeEmptyElementsFromArray(nums); // type is number[] |
@pie6k As far as I can tell that's not a solution, that's merely your assertion ( |
@pie6k That Here's a cleaned up version that has no need for type assertions:
|
@robertmassaioli, export function hasPresentKey<K extends string | number | symbol>(k: K) {
return function <T, V>(
a: T & { [k in K]?: V | null }
): a is T & { [k in K]: V } {
return a[k] !== undefined && a[k] !== null;
};
} |
FWIW, I have seen somebody using interface Foo {
type: "foo";
}
interface Bar {
type: "bar";
}
declare const fooBar: (Foo | Bar)[];
const foos = fooBar.flatMap(v => v.type === "foo" ? [v] : []); // inferred as Foo[]
const bars = fooBar.flatMap(v => v.type !== "foo" ? [v] : []); // inferred as Bar[] Basically all you need to do is to append (Edit: Expanding the comments, this is already mentioned here.) |
Yes this is very annoying when chaning methods and/or check for more than just nullish. type MyObj = { data?: string };
type MyArray = { list?: MyObj[] }[];
const myArray: MyArray = [];
const result = myArray
.map((arr) => arr.list)
.filter((arr) => arr && arr.length)
.map((arr) => arr
// ^^^ Object is possibly 'undefined'.
.filter((obj) => obj && obj.data)
.map(obj => JSON.parse(obj.data))
// ^^^^^^^^ Type 'undefined' is not assignable to type 'string'.
); The nullish type guard can handle it if directly set as function. But not in the case above. const isNotNullish = <T>(input: T | null | undefined): input is T => {
return input !== null && input !== undefined
}
// Work
array.filter(isNotNullish);
// Not work
array.filter(item => isNotNullish(item) && item.length)
array.filter(item => item?.length) Now I could write a custom type guard like isNotNullish. Something like isArrayFilled to also check for length. Same to test property on object (in this case the "data"). I could also use the not-null assertion |
@infacto You could also just add a type annotation. |
@MartinJohns All variables are well typed (maybe some in context in the example above). Do you mean casting or use not-null assertion? Then I would either break the chain and move the first result in a new not-null variable. Otherwise I have to do it (cast, assert) everywhere again and again. I just wanted another example for this topic here (Infer type guard from filter). But I understand the problem. Not sure if TypeScript can fix that. Because the callback must explicitly set the return type like a type-guard does. And TS does not know that's inside the callback. But the TS lang devs know it better for sure. Idk. Spontaneous ideas (not tested): Wrap filter fn in a type-guard fn or use an own custom function to filter array with condition fn which returns type guarded type (input is T from T | undefined | null or unknown). Something like |
@infacto I think maybe @MartinJohns meant typing filter predicate as a typeguard like this type MyObj = { data?: string };
type MyArray = { list?: MyObj[] }[];
const myArray: MyArray = [];
const result = myArray
.map((arr) => arr.list)
.filter((arr): arr is MyObj[] => !!(arr && arr.length))
.map((arr) => arr
.filter((obj): obj is Required<MyObj> => !!(obj && obj.data))
.map(obj => JSON.parse(obj.data))
); Second example array.filter((item): item is WhateverTheTypeOfArrayItemIs[] => item?.length) But I want to admit I don't like this option |
@infacto Actually we can make first example work with current state of things using some more complex typeguards Playground type MyObj = { data?: string };
type MyArray = { list?: MyObj[] }[];
const myArray: MyArray = [];
const isNotNullish = <T>(input: T | null | undefined): input is T => input != null
const isNotEmpty = <T>(input: T[]) => input.length !== 0;
function both<I, O1 extends I>(
predicate1: (input: I) => boolean,
predicate2: (input: I) => input is O1,
): (input: I) => input is O1;
function both<I, O1 extends I>(
predicate1: (input: I) => input is O1,
predicate2: (input: O1) => boolean,
): (input: I) => input is O1;
function both<I, O1 extends I, O2 extends O1>(
predicate1: (input: I) => input is O1,
predicate2: (input: O1) => input is O2,
): (input: I) => input is O2;
function both<I>(
predicate1: (input: I) => boolean,
predicate2: (input: I) => boolean,
) {
return (input: I) => predicate1(input) && predicate2(input)
}
type HasNonNullishPredicate<P extends string> = <T extends { [K in P]?: unknown }>(obj: T) => obj is T & { [K in P]: Exclude<T[K], undefined | null> };
function hasNonNullish<P extends string>(prop: P): HasNonNullishPredicate<P>;
function hasNonNullish(prop: string) {
return (input: object) => prop in input && (input as any).prop != null; // this `as any` cast will be unnecessary in TSv4.9
}
const result = myArray
.map((arr) => arr.list)
.filter(both(isNotNullish, isNotEmpty))
.map((arr) => arr
.filter(hasNonNullish('data'))
.map(obj => JSON.parse(obj.data))
); |
@temoncher Thanks for your input. Helpful stuff. Somehow I hadn't thought of the return value of the arrow function in the filter method. 🤦♂️ Only indirect. Yes, makes sense. ^^ But anyway, it would be great if TypeScript could just infer that for less and cleaner code. |
Sorry if I disturb you. I'm facing this problem again, where infer type guard for filter function would be awesome. Just another contribution post or thinking out loud. To the example above: context.filter((obj): obj is Required<MyObj> => !!obj?.data) This guards the objects to have all properties required. In this example, it's ok. But for more complex object with several (optional) properties should rather do that. context.filter((obj): obj is MyObj & Required<Pick<MyObj, 'data'>> => !!obj?.data) Pick only the tested properties. Otherwise you may obfuscate nullish properties. It's a really long johnny. Infer type would simply look like: context.filter((obj) => !!obj?.data) Now I craft many type guards to avoid such long complex type defs. I mean this is just a simple example. The real world code type def would be 2 miles of code horizontal scoll. I thinking of a chain guard-type function like rxjs pipes. But it's either exhausting or unsafe (e.g. simple cast from unknown by assuming types). I don't want to rule out that I'm thinking too complicated and that there is a much simpler way. I just want to fix strict type issues. In best case without blind casting. |
@infacto in pretty sure that's what isPresentKey does from ts-is-present |
This is pretty annoying issue that happens quite often. Can we raise priority on this one? |
Leaving https://github.com/total-typescript/ts-reset/ here as a drop-in "fix" until this lands in TS itself. |
i hope anyone advocating for using function isPresent<T>(input: null | undefined | T): input is T {
return !input;
}
const arr = ["string", 8, null, undefined].filter(isPresent);
// expects ["string", 8]
// but is instead [null, undefined]
console.log(arr)
|
Hi all, When will TypeScript's type inference be fixed to handle this simple example ? // Types as `(o: string | undefined) => o is string`
const myGuard = (o: string | undefined): o is string => !o;
// Types as `(o: string | undefined) => boolean` but should type as `(o: string | undefined) => o is string`
const mySecondGuard = (o: string | undefined) => myGuard(o); |
Thank you @danvk and @RyanCavanaugh! |
@ptitjes The issue with your example is that the first guard is not correct: const myGuard = (o: string | undefined): o is string => !o;
const emptyString = '';
if(!myGuard(emptyString)) {
// according to the "if and only if" nature of type predicates,
// the fact that the predicate failed means that `emptyString`
// _must not_ be a string, but actually it is.
} |
i use such a patch: diff --git a/node_modules/typescript/lib/lib.es5.d.ts b/node_modules/typescript/lib/lib.es5.d.ts
index a88d3b6..f091ecb 100644
--- a/node_modules/typescript/lib/lib.es5.d.ts
+++ b/node_modules/typescript/lib/lib.es5.d.ts
@@ -1268,6 +1268,7 @@ 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<F extends BooleanConstructor>(predicate: F, thisArg?: any): Exclude<T, false | null | undefined | '' | 0>[];
/**
* 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.
@@ -1459,6 +1460,7 @@ 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<F extends BooleanConstructor>(predicate: F, thisArg?: any): Exclude<T, false | null | undefined | '' | 0>[];
/**
* 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.
with [].filter(Boolean) |
TypeScript Version: 2.3
Code
with
strictNullChecks
enabled.Expected behavior:
This should type check. The type of
evenSquares
should benumber[]
.Actual behavior:
The type of
evenSquares
is deduced as(number|null)[]
, despite the null values being removed via the call to.filter
.The text was updated successfully, but these errors were encountered: