Why can’t TypeScript infer type from filtered arrays?

A few things are going on here.


The first (minor) issue is that, with your Student interface, the compiler will not treat checking the isValid property as a type guard:

const s = students[Math.random() < 0.5 ? 0 : 1];
if (s.isValid) {
    foo([s]); // error!
    //   ~
    // Type 'Student' is not assignable to type 'ValidStudent'.
}

The compiler is only able to narrow the type of an object when checking a property if the object’s type is a discriminated union and you are checking its discriminant property. But the Student interface is not a union, discriminated or otherwise; its isValid property is of a union type, but Student itself is not.

Luckily, you can get a nearly equivalent discriminated union version of Student by pushing the union up to the top level:

interface BaseStudent {
    name: string;
}
interface ValidStudent extends BaseStudent {
    isValid: true;
}
interface InvalidStudent extends BaseStudent {
    isValid: false;
}
type Student = ValidStudent | InvalidStudent;

Now the compiler will be able to use control flow analysis to understand the above check:

if (s.isValid) {
    foo([s]); // okay
}

This change is not of vital importance, since fixing it does not suddenly make the compiler able to infer your filter() narrowing. But if it were possible to do this, you’d need to use something like a discriminated union instead of an interface with a union-valued property.


The major issue is that TypeScript does not propagate the results of control flow analysis inside a function implementation to the scope where the function is called.

function isValidStudentSad(student: Student) {
    return student.isValid;
}

if (isValidStudentSad(s)) {
    foo([s]); // error!
    //   ~
    // Type 'Student' is not assignable to type 'ValidStudent'.
}

Inside isValidStudentSad(), the compiler knows that student.isValid implies that student is a ValidStudent, but outside isValidStudentSad(), the compiler only knows that it returns a boolean with no implications on the type of the passed-in parameter.

One way to deal with this lack of inference is annotate such boolean-returning functions as a user-defined type guard function. The compiler can’t infer it, but you can assert it:

function isValidStudentHappy(student: Student): student is ValidStudent {
    return student.isValid;
}
if (isValidStudentHappy(s)) {
    foo([s]); // okay
}

The return type of isValidStudentHappy is a type predicate, student is ValidStudent. And now the compiler will understand that isValidStudentHappy(s) has implications for the type of s.

Note that it has been suggested, at microsoft/TypeScript#16069, that perhaps the compiler should be able to infer a type predicate return type for the return value of student.isValid. But it’s been open for a long time and I don’t see any obvious sign of it being worked on, so for now we can’t expect it to be implemented.

Also note that you can annotate arrow functions as user-defined type guards… the equivalent to isValidStudentHappy is:

const isValidStudentArrow = 
  (student: Student): student is Student => student.isValid;

We’re almost there. If you annotate your callback to filter() as a user-defined type guard, a wonderful thing happens:

const validStudents = 
  students.filter((student: Student): student is ValidStudent => student.isValid);

foo(validStudents); // okay!

The call to foo() type checks! That’s because the TypeScript standard library typings for Array.filter() were given a new call signature that will narrow arrays when the callback is a user-defined type guard. Hooray!


So this is about the best that the compiler can do for you. The lack of automatic inference of type guard functions means that at the end of the day you are still telling the compiler that the callback does the narrowing, and is not much safer than the type assertion you’re using in the question. But it is a little safer, and maybe someday the type predicate will be inferred automatically.

Playground link to code

Leave a Comment