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

Improve type guard inference when using callback functions. #18572

Closed
mpawelski opened this issue Sep 19, 2017 · 4 comments
Closed

Improve type guard inference when using callback functions. #18572

mpawelski opened this issue Sep 19, 2017 · 4 comments
Labels
Duplicate An existing issue was already created

Comments

@mpawelski
Copy link

SUGGESTION
TypeScript Version: 2.5.2

Problem

I think type guards can be improved so that we don't need to define separate named function when we can provide lambda function.
Right now we don't need to abstract typeof and instanceof expressions into a function when we are dealing with single variable. For example:

function getStringOrNumber(): string | number {
    return "foo";
}

let a = getStringOrNumber();
if (typeof a === "string") {
    a.concat("b") // "a" is string
} else {
    a.valueOf(); // "a" is number
}

// ------------------- //

abstract class Animal {
}
class Dog extends Animal {
    woof(): string { return "wooof" }
}
class Cat extends Animal {
    meow(): string { return "meow" }
}

let animal: Animal;
if (animal instanceof Dog) {
    animal.woof(); // "animal" is Dog
} else if (animal instanceof Cat) {
    animal.meow() // "animal" is Cat
}

I expected to get similar behavior when using Array.filter method (the one with this signature: filter<S extends T>(callbackfn: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[];):

var arrayOfStringOrNumber = ["foo", 1]; // (string | number)[]

let arrayOfStrings = arrayOfStringOrNumber.filter(item => typeof item === "string"); // (string | number)[]
let arrayOfNumbers = arrayOfStringOrNumber.filter(item => typeof item === "number"); // (string | number)[]

//-------------//
var animals: Animal[] = [new Dog(), new Cat()];

var dogs = animals.filter(a => a instanceof Dog); // Animal[]
var cats = animals.filter(a => a instanceof Cat); // Animal[]

Expected behavior:
filter result is array of narrowed type (string[], number[], Dog[], Cat[])
Actual behavior:
filter result is array of the same type as original array ((string | number)[] and Animal[])

Proposed solutions

I have two proposals:

First

(preferable)

Improve type inference for function return type when using typeof and instanceof expression. So this code

function isString(item: any) {
    return typeof item === "string";
}

function isNumber(item: any) {
    return typeof item === "number";
}

function isCat(animal: any) {
    return animal instanceof Cat;
}

would not get return type boolean but item is string, item is number and item is Cat

Second

(I'm not sure if I explained it properly)

When having anonymous function that is contextually typed to be type guard function and this function use typeof or instanceof expressions then it should infer the return type be type guard return type (arg is type).

Current behavior:

let func1: <T, S extends T>(arg: T) => arg is S = (arg : Animal) => arg instanceof Dog;
let func2: <T, S extends T>(arg: T) => arg is S = (arg : string | number) => typeof arg === "string";

Expected behavior:
It should "just work" and have return type be inferred as arg is Animal and arg is string .
Actual behavior:
We get error:

'Type '(arg: Animal) => boolean' is not assignable to type '<T, S extends T>(arg: T) => arg is S'.
Signature '(arg: Animal): boolean' must be a type predicate.'
'Type '(arg: string | number) => boolean' is not assignable to type '<T, S extends T>(arg: T) => arg is S'.
Signature '(arg: string | number): boolean' must be a type predicate.'

Right now we need to explicitly say this anonymous function is type guard function.:

let func: <T, S extends T>(arg: T) => arg is S = (arg : Animal) : arg is Dog => arg instanceof Dog;
let func2: <T, S extends T>(arg: T) => arg is S = (arg : string | number) : arg is string => typeof arg === "string";

let arrayOfStrings = arrayOfStringOrNumber.filter(<(arg: any) => arg is string>(item => typeof item === "string")); 
let arrayOfNumbers = arrayOfStringOrNumber.filter(<(arg: any) => arg is number>(item => typeof item === "number"));
@Bnaya
Copy link

Bnaya commented Sep 19, 2017

Is will be very nice if its possible

@RyanCavanaugh
Copy link
Member

Same as #10734 ?

@mhegazy
Copy link
Contributor

mhegazy commented Sep 19, 2017

Looks like a duplicate of #14891 and #5101 (comment)

@mhegazy mhegazy added the Duplicate An existing issue was already created label Sep 19, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Oct 4, 2017

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@mhegazy mhegazy closed this as completed Oct 4, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

4 participants