Type Narrowing an element in an array with `find`

Viewed 1055

If I have an array filled with items of a union type and I want to find an element within the array and work with it, knowing from my find that I have narrowed the type, how do I accomplish this?

That is, if I have the following types:

interface A {
  text: string;
}
interface B {
  type: string;
}
type C = A|B;

and the following code

const arr: Array<C> = [....]

I want to find the first element of arr that has a type attribute.

Logically, I think this is represented by

const output = arr.find(child => {
  if ('type' in child) return true;
  return false
})

This seems like it should narrow the type of output to be B, but it doesn't. The compiler still seems to think that output is of type C.

4 Answers

The type safe way to implement the type guard in your case is

function isB(c: C): c is B {
    if ('type' in c) { 
        return typeof c.type === 'string';
    }

    return false;
}

The other answer does not take into account the fact that there are valid C values that have type property that is not a string.

use a typeguard:

function isB(c: C) c is B {
   return typeof (c as B).type === 'string';
}


const output = arr.find(isB);

the typeguard tells TypeScript what's going on so it can infer the type.

edited to check the type of type is string, since apparently that's possible

In further searching, I stumbled across https://github.com/microsoft/TypeScript/issues/20218

From there, I found the comment

a => isCat(a) isn't explicitly annotated as being a type predicate, and we don't infer those.

As the others state, the solution is to use an explicit type guard as the predicate function. replacing the arrow function with some user-defined type guard isC fixes the problem.

Since the other answers here seem to focus more on how to make absolutely sure that an object does in fact satisfy the interface B, I will try to answer the question of how to propagate the narrowing in the callback to the return value of find:

Actually @zerkms and @bryan60 seem to be correct in that a type guard is needed, but all you need to add to your code is the type predicate child is B like this:

const output = arr.find((child): child is B => {
  if ('type' in child) return true;
  return false
})

Beware, however, that this is you telling the compiler what type the child must be whenever the callback returns a truthy value. So the narrowing that the compiler does for you inside the callback is NOT actually propagated, and if you later change the interfaces, or the type C, or the logic inside the callback, this predicate will keep assuring the compiler that output is either B or undefined, which may or may not be true.

Related