The Wayback Machine - https://web.archive.org/web/20201101031325/https://github.com/microsoft/TypeScript/issues/16069
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

Infer type guard => array.filter(x => !!x) should refine Array<T|null> to Array<T> #16069

Open
danvk opened this issue May 24, 2017 · 30 comments
Open

Comments

@danvk
Copy link

@danvk danvk commented May 24, 2017

TypeScript Version: 2.3

Code

const evenSquares: number[] =
    [1, 2, 3, 4]
        .map(x => x % 2 === 0 ? x * x : null)
        .filter(x => !!x);

with strictNullChecks enabled.

Expected behavior:

This should type check. The type of evenSquares should be number[].

Actual behavior:

The type of evenSquares is deduced as (number|null)[], despite the null values being removed via the call to .filter.

@mhegazy
Copy link

@mhegazy mhegazy commented May 24, 2017

For filter specifically, #7657 would be something we can do here. but that still requires you to write an explicit type guard annotation somewhere, (x: number| null): x is number somewhere.
the other part is the compiler figuring out that type guard automatically from the function body, but that is not a trivial, #5101 tracks some aspects of that.

@RyanCavanaugh
Copy link
Member

@RyanCavanaugh RyanCavanaugh commented Jun 29, 2017

Reopening to track this since both other issues now target lib.d.ts changes

@dgreene1
Copy link

@dgreene1 dgreene1 commented Oct 27, 2017

Subscribing since this is a common pattern I use after a map

@AlexGalays
Copy link

@AlexGalays AlexGalays commented Nov 24, 2017

Just chiming in to say that this shouldn't be fixed just for Arrays.
This is very useful for all custom (container or not) types too.

@dgreene1
Copy link

@dgreene1 dgreene1 commented Feb 28, 2018

Here's a workaround:

// I wish I could just do the following:
let result = ["", 3,null, undefined].filter(x => x != null);

Instead, you can do this:

// Create this helper function
function isNotNullOrUndefined<T extends Object>(input: null | undefined | T): input is T {
    return input != null;
}
// This actually determines that result is of type (string|number)[]
let result = ["", 3,null, undefined].filter(isNotNullOrUndefined);

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 input is T then it allows filter to utilize the control flow features of the type checking to discriminate the union.

###################################################

That being said, I hope that we can get this type guard inference into the language. :)

@mhegazy mhegazy changed the title array.filter(x => !!x) should refine Array<T|null> to Array<T> Infer type guard => array.filter(x => !!x) should refine Array<T|null> to Array<T> May 2, 2018
@benschulz
Copy link

@benschulz benschulz commented May 2, 2018

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).

@scott-ho
Copy link

@scott-ho scott-ho commented May 25, 2018

@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?

@dgreene1
Copy link

@dgreene1 dgreene1 commented May 25, 2018

@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 pipe() and filter() are sending you. Since the filter I know is the filter that exists on arrays, and since the snippet above doesn't have filter on an array, then I don't think it's the filter that pertains to this TypeScript issue. I.e. it's not the standard ES5 filter, it's rxJs' filter.

@scott-ho
Copy link

@scott-ho scott-ho commented May 28, 2018

@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));
  };
}
@pie6k
Copy link

@pie6k pie6k commented Aug 7, 2019

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[]
@samhh
Copy link

@samhh samhh commented Aug 7, 2019

@pie6k As far as I can tell that's not a solution, that's merely your assertion (as T[]) unsafely narrowing.

@MartinJohns
Copy link
Contributor

@MartinJohns MartinJohns commented Aug 7, 2019

@pie6k That [null, undefined].includes(..) will always create a new array, won't it?

Here's a cleaned up version that has no need for type assertions:

function hasValue<T>(value: T | undefined | null): value is T {
    return value !== undefined && value !== null;
}

function removeEmptyElementsFromArray<T>(array: Array<T | undefined | null>): T[] {
    return array.filter(hasValue);
}
@pie6k
Copy link

@pie6k pie6k commented Aug 8, 2019

@samhh it's narrowing, but it's doing it safely as we're removing empty values before

@samhh
Copy link

@samhh samhh commented Aug 8, 2019

@pie6k It's not safe. You can prove this like so:

export function removeEmptyElementsFromArray<T>(array: Array<T | Empty>): T[] {
  return array as T[];
}

This still compiles just as yours did. It's the assertion that's pleasing the compiler; you can remove the isEmpty function's type guard as it's not doing anything. Your safety is not coming from the type system, but it should do.

I don't think there's any way to generically type guard a negative, which is what your example needs to drop the assertion.

@benschulz
Copy link

@benschulz benschulz commented Aug 9, 2019

I believe you're talking past one another. removeEmptyElementsFromArray as posted by @pie6k and @MartinJohns are sound. However, small changes to the code may break soundness without warning because the compiler is blinded by explicitly added type assertions. That's what @samhh is getting at.

This code issue is about inferring type guards and therefore I have to agree with @samhh that the code above is not a solution: You're still manually adding type annotations.

jablko added a commit to jablko/TypeScript that referenced this issue Nov 15, 2019
@mikeyhew
Copy link

@mikeyhew mikeyhew commented Nov 15, 2019

Referencing #18122, because this is what is required for the example code there to work, and I can't comment there because it's closed locked.

@kasperpeulen
Copy link

@kasperpeulen kasperpeulen commented Nov 22, 2019

This is quite a clean solution for this problem, using type predicates:
http://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates

function isNotNull<T>(it: T): it is NonNullable<T> {
  return it != null;
}

const nullableNumbers = [1, null];
const nonNullableNumbers = nullableNumbers.filter(isNotNull);
@eduter
Copy link

@eduter eduter commented Nov 22, 2019

@kasperpeulen, if you make this mistake, it still type-checks:

function isNotNull<T>(it: T): it is NonNullable<T> {
  return it === null; // oops...
}

Using a callback with a type predicate is not much safer than using a type assertion on the return value.

And using an arrow function does not look clean at all:

nullableItems.filter((it): it is NonNullable<typeof it> => it !== null)
@AlexGalays
Copy link

@AlexGalays AlexGalays commented Nov 22, 2019

This is quite a clean solution for this problem, using type predicates:
http://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates

function isNotNull<T>(it: T): it is NonNullable<T> {
  return it != null;
}

const nullableNumbers = [1, null];
const nonNullableNumbers = nullableNumbers.filter(isNotNull);

This is not a solution to this problem since this problem is all about inferring the type guard.

@kasperpeulen
Copy link

@kasperpeulen kasperpeulen commented Nov 22, 2019

I wrote this for people that wonder how they can filter all nulls from an array in a way that typescript understand. If you use this isNotNull as a utility function, then I think just writing nullableNumbers.filter(isNotNull) is quite clean.

However, I totally agree that it would be awesome if typescript can infer any type predicates itself.

@robertmassaioli
Copy link

@robertmassaioli robertmassaioli commented Dec 14, 2019

To avoid everybody having to write the same helper functions over and over again I bundled isPresent, isDefined and isFilled into a helper library: npm

When Typescript bundles this functionality in I'll remove the package.

To use the library:

import { isPresent, isDefined, isFilled } from 'ts-is-present';

arrayWithUndefinedAndNullValues.filter(isPresent)
arrayWithUndefinedValues.filter(isDefined)
arrayWithNullValues.filter(isFilled)

To see why this package works please read the readme of the NPM package.

@pie6k
Copy link

@pie6k pie6k commented Dec 16, 2019

Wow! @robertmassaioli this is cool!

image

function isPresent<T>(t: T | undefined | null | void): t is T {
  return t !== undefined && t !== null;
}

const foo: Array<number | null> = [2,3, null, 4];

const bar = foo.filter(isPresent); // number[]
@jtomaszewski
Copy link

@jtomaszewski jtomaszewski commented Jul 30, 2020

Similar to isPresent from ts-is-present library, me and @Smtih just created this utility hasPresentKey fn that helps you filter items down to the ones that have defined non-null item at a[k]. Maybe you'll find it useful.

Source:

/**
 * Builds a function that can be used to filter down objects
 * to the ones that have a defined non-null value under a key `k`.
 *
 * @example
 * ```ts
 * const filesWithUrl = files.filter(file => file.url);
 * files[0].url // In this case, TS might still treat this as undefined/null
 *
 * const filesWithUrl = files.filter(hasPresentKey("url"));
 * files[0].url // TS will know that this is present
 * ```
 */
export function hasPresentKey<
  V,
  K extends string | number | symbol,
  T extends {}
>(
  k: K
): (a: T & { [k in K]?: V | undefined | null }) => a is T & { [k in K]: V };
export function hasPresentKey<
  V,
  K extends string | number | symbol,
  T extends {}
>(k: K): (a: T & { [k in K]?: V | undefined }) => a is T & { [k in K]: V } {
  return (a): a is T & { [k in K]: V } => a[k] !== undefined && a[k] !== null;
}

P.S. @robertmassaioli maybe we could add it to ts-is-present library? functions like hasFilledKey hasDefinedKey hasPresentKey? Should I make a PR against https://bitbucket.org/echo_rm/ts-is-present ?

@robertmassaioli
Copy link

@robertmassaioli robertmassaioli commented Aug 1, 2020

@jtomaszewski That looks like a good addition to ts-is-present please raise a PR against that repository with the code changes and applicable docs and we'll get this out the door. Cheers!

Update from 21/Sep/2020: Okay, it seems that the above functions don't quite typecheck the way that we would like them to. PR in which we are investigating further here: robertmassaioli/ts-is-present#1

@jtomaszewski
Copy link

@jtomaszewski jtomaszewski commented Aug 18, 2020

Here's what bitbucket says to me when I'm trying to open a PR to your repo:
image

Any chance moving it to GH?

@jtomaszewski
Copy link

@jtomaszewski jtomaszewski commented Aug 18, 2020

Anyways, I added the code in ailohq/ts-is-present@1f0457f . Feel free to look it up.

Strangely I have some problems with ts-jest compiling the types. See below what it prints. Strangely, if we copy paste the code into our repo, it works well, and tsc doesnt raise any errors... Unfortunately I don't have time now to debug it more closely.

image

@robertmassaioli
Copy link

@robertmassaioli robertmassaioli commented Aug 23, 2020

@jtomaszewski I have moved the repository to Github. I want you to get credit in the commits so please feel free to try again there: https://github.com/robertmassaioli/ts-is-present

@graup
Copy link

@graup graup commented Sep 28, 2020

Is this also the issue tracking that I would expect these two lines to be the same?

const values = [4, "hello"] as const;
const result1 = values.filter(val => typeof val === 'number');  // (4 | "hello")[]
const result2 = values.flatMap(val => typeof val === 'number' ? val : []);  // 4[]

I understand the issue is that predicate-like functions aren't inferred as type guards, so any type narrowing that might happen inside it isn't available to the context? In this example, of course I can write is number, but that doesn't work with more complex union types. I'd really like to use the inference (which is already happening anyway).

@AlexGalays
Copy link

@AlexGalays AlexGalays commented Sep 28, 2020

@graup Seems like the same issue yes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.