Skip to content

Type guards in Array.prototype.filter #7657

Description

TypeScript Version:

nightly (1.9.0-dev.20160323)

Code

let numbers: number[] =
    [1, '2', 3, '4'].filter(x => typeof x !== 'string');

declare function isNumber(arg: any): arg is number;
let numbers2: number[] = [1, '2', 3, '4'].filter(isNumber);

Playground

Expected behavior:
.filter with a type guard returns an array of the specified type. This would be especially useful with --strictNullChecks, so we could do something like foo.map(maybeReturnsNull).filter(x => x != null)....

Actual behavior:
.filter returns an array of the same type, and the two lets above are errors.

Activity

  1. added
    SuggestionAn idea for TypeScript
    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.
    Domain: lib.d.tsThe issue relates to the different libraries shipped with TypeScript
    and removed
    Needs ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.
    on Mar 23, 2016
  2. RyanCavanaugh commented on Mar 23, 2016

    @RyanCavanaugh
    Member

    The required overload is

    interface Array<T> {
        filter<U extends T>(pred: (a: T) => a is U): U[];
    }

    (which you can add to your codebase today to make this work)

  3. calebegg commented on Mar 23, 2016

    @calebegg
    Author

    Wow, I didn't realize it would be that easy. It doesn't fix the first case (presumably because the function is not getting recognized as a type guard function), but it does work with a cast:

    let numbers: number[] =
        [1, '2', 3, '4']
            .filter(<(x) => x is number>(x => typeof x == 'number'));

    Would inferring that x => typeof x == 'number' is a type guard be a good candidate for a separate suggestion? Or is it too specific? (I didn't see anything in the tracker already).

  4. DanielRosenwasser commented on Mar 24, 2016

    @DanielRosenwasser
    Member

    The problem I ran into with adding the overload into lib.d.ts was that it doesn't work with Array.isArray, and I don't entirely recall why. We could investigate this a bit.

  5. NoelAbrahams commented on Mar 24, 2016

    @NoelAbrahams

    Looks to be the same as #2835

  6. RyanCavanaugh commented on Mar 28, 2016

    @RyanCavanaugh
    Member

    (context: #2835 was long before we had type guards)

  7. bcherny commented on Mar 30, 2016

    @bcherny

    if someone can patch this to lib.d.ts, i could get rid of so many asserts in my code.

  8. added this to the milestone on Apr 11, 2016
  9. loilo commented on Jun 8, 2016

    @loilo

    Is there some place to read up about the "is" in Ryan Cavanaugh (@RyanCavanaugh)'s overloading code above? Couldn't find it anywhere, neither in this issue about reserved keywords nor on the pages linked from there.
    Also Google wasn't helpful this time.

  10. 30 remaining items

  11. josundt commented on Nov 13, 2016

    @josundt

    I have tested this with TS 2.1.1.
    While ReadOnlyArray.filter with type guard functions work as expected, it seems it does not work with Array.filter.

    const roArr: ReadonlyArray<string | null> = ["foo", null];
    const fRoArr = roArr.filter((i): i is string => typeof i === "string"); 
    // -> fRoArr inferred as string[]
    
    const arr: Array<string | null> = ["foo", null];
    const fArr = arr.filter((i): i is string => typeof i === "string");
    // -> fArr inferred as (string | null)[]
  12. Arnavion commented on Nov 13, 2016

    @Arnavion
    Contributor
  13. OliverJAsh commented on Nov 25, 2016

    @OliverJAsh
    Contributor

    How can you use type predicates to negate a type? For example:

    [1, 'foo', true].filter((x): x is not number => typeof x !== 'number')
  14. janslow commented on Nov 25, 2016

    @janslow

    Not ideal, but this works for primitives. Although, it might be easier to leave that unguarded.

    type NotNumber = Object | string | symbol | undefined | null | boolean | Function;
    function notNumber(x: any): x is NotNumber {
        return typeof x !== 'number';
    }
  15. bcherny commented on Dec 7, 2016

    @bcherny

    Any ideas where discriminated unions will/can help here, or do we have to provide user defined type guards?

    Mohamed Hegazy (@mhegazy) Ryan Cavanaugh (@RyanCavanaugh) Possible for one of you guys to chime in here? It's a lot of friction to have to (a) explicitly parametrize or (b) explicitly type guard every .filter call.

  16. gasi commented on Mar 8, 2017

    @gasi

    I tried the type guard as follows with no luck:

    type FooBar = Foo | Bar
    
    interface Foo {
        type: 'Foo'
    }
    
    interface Bar {
        type: 'Bar'
    }
    
    const fooBars: Array<FooBar> = [
        { type: 'Foo' },
        { type: 'Bar' },
    ]
    
    const foos: Array<Foo> = fooBars.filter(
        (item): item is Foo => item.type === 'Foo'
    )
  17. mvestergaard commented on Apr 20, 2017

    @mvestergaard

    Is this related to this problem? Make sure to enable strictNullChecks.

    I would expect the control flow here to know that in the .map, the value is number and not undefined.
    I find that this problem forces me to write much less readable code, because I have to jump through hoops to get around the type system tripping up.

    If this is the same issue, is there any plans to make this scenario work in the near-ish future?

  18. maiermic commented on Jul 17, 2017

    @maiermic

    This issue has been fixed by #16223 and can be closed. You can try it out in typescript@2.5.0-dev.20170712 for example.

  19. calebegg commented on Sep 15, 2017

    @calebegg
    Author

    Michael Maier (@maiermic): Are you sure? I still get an error on this code:

    ['a', undefined].filter((e): e is string => !!e)[0].substring(0, 10);
    test.ts(1,1): error TS2532: Object is possibly 'undefined'.
    

    Playground (be sure to check "strictNullChecks" in options to see the error)

  20. maiermic commented on Sep 17, 2017

    @maiermic

    Caleb Eggensperger (@calebegg) That looks like a bug to me. Your example works if you use a function declaration instead of the lambda:

    ['a', undefined].filter(isString)[0].substring(0, 10);
    
    function isString(e): e is string {
        return !!e
    }
  21. NaridaL commented on Sep 18, 2017

    @NaridaL
    Contributor

    Caleb Eggensperger (@calebegg) I opened #18562 ... in the meantime, specifying the type of e explicitly is a workaround:

    ['a', undefined].filter((e: any): e is string => !!e)[0].substring(0, 10);
                              ^^^^^
  22. calebegg commented on Sep 18, 2017

    @calebegg
    Author

    Adrian Leonhard (@NaridaL) Interesting, thanks for identifying the issue!

  23. removed this from the milestone on Apr 26, 2018
  24. locked and limited conversation to collaborators on Jul 31, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Domain: lib.d.tsThe issue relates to the different libraries shipped with TypeScriptHelp WantedYou can do thisSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions