-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Infer type guard => array.filter(x => !!x) should refine Array<T|null> to Array<T> #16069
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: