Below is some sample code. TypeScript infers the type of validStudents
as Students[]
. It should be obvious to anyone reading the code that, because all invalid records were filtered out, validStudents
can safely be considered to have a type of ValidStudents[]
.
interface Student {
name: string;
isValid: boolean;
}
type ValidStudent = Student & { isValid: true };
const students: Student[] = [
{
name: 'Jane Doe',
isValid: true,
},
{
name: "Robert'); DROP TABLE Students;--",
isValid: false,
}
];
const validStudents = students.filter(student => student.isValid);
function foo(students: ValidStudent[]) {
console.log(students);
}
// the next line throws compile-time errors:
// Argument of type 'Student[]' is not assignable to parameter of type 'ValidStudent[]'.
// Type 'Student' is not assignable to type 'ValidStudent'.
// Type 'Student' is not assignable to type '{ isValid: true; }'.
// Types of property 'isValid' are incompatible.
// Type 'boolean' is not assignable to type 'true'.ts(2345)
foo(validStudents);
It's possible to make this code work by adding a type assertion:
const validStudents = students.filter(student => student.isValid) as ValidStudent[];
... but it feels a little hacky. (Or maybe I just trust the compiler more than I do myself!)
Is there a better way to handle this?
In Typescript, Filter() is a built-in array method which is defined as a method for creating a new array or set of elements that contains a subset of the given array elements by returning the array of all the values of the elements in the newly created sub-array over the given array.
filter() does not modify the original array.
filter() does not mutate the array on which it is called. The range of elements processed by filter() is set before the first invocation of callbackFn .
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.
TypeScript uses the best common type algorithm to select the best candidate types that are compatible with all variables. TypeScript also uses contextual typing to infer types of variables based on the locations of the variables.
To get th element type from an array type, use a condition type with an infer declaration to infer the type of an element in the array. TypeScript will fill in the type of the element, and we can return it in the true branch of the conditional type. Copied! type ArrElement<ArrType> = ArrType extends readonly (infer ElementType)[] ?
Or anything else that doesn't change the composition of the array, hence it cannot really narrow the type. Also, if you filter an empty array, your validity filter still produces an array of invalid students. And an array of valid students. See Also Way to tell TypeScript compiler Array.prototype.filter removes certain types from an array?
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With