Typescript: map union type to a single type

6.2k views Asked by At

I am trying to narrow down (w/ inference) the type that I want out of this array filter, but it's giving me an TypeError: 'Item' is missing the following properties

type ItemList = (Item | ItemGroup )[];
type ItemGroup = {
  name: string;
  items: Item[];
}
type Item = {
  key: string;
  label: string;
}

const list: ItemList = [
   {
      key: 'key',
      label: 'label'
   },
   {
      name: 'name',
      items: [
        {
          key: 'key1',
          label: 'label2'
        },
        {
          key: 'key3',
          label: 'label4'
        },
      ]
   }
]

const groups: ItemGroup[] = list.filter( l => 'name' in l )
      ^^^^^^
// Type '(Item | ItemGroup)[]' is not assignable to type 'ItemGroup[]'.
//   Type 'Item | ItemGroup' is not assignable to type 'ItemGroup'.
//     Type 'Item' is missing the following properties from type 'ItemGroup': name, items ts(2322)

Any ideas?

3

There are 3 answers

3
jcalz On BEST ANSWER

Unfortunately the compiler isn't smart enough to look at the l => "name" in l callback and understand that it can be used to narrow an Item | ItemGroup to just an ItemGroup. Luckily, you can tell the compiler that this is the intent by annotating it as a user-defined type guard function:

const isItemGroup = (l: Item | ItemGroup): l is ItemGroup => "name" in l;

Now if you call isItemGroup(l) and the result is true, the compiler will understand that l is an ItemGroup. Additionally, the standard library provides a call signature for Array.prototype.filter() that accepts a user-defined type guard callback and produces a narrowed array. So by using isItemGroup as the callback, you get your desired outcome:

const groups: ItemGroup[] = list.filter(isItemGroup); // no error now

Playground link to code

1
Linda Paiste On

You have a array which contains both Item and ItemGroup elements. You want to filter this array to just the elements which are ItemGroup, and you want typescript to understand that you have filtered the list and know that the returned type is ItemGroup[].

How you can achieve this is buy turning the filter l => 'name' in l into its own type guard function. The return type value is ItemGroup tells typescript "if and only if this is true, value is of type ItemGroup".

const isItemGroup = (value: Item | ItemGroup): value is ItemGroup => 'name' in value;

const groups: ItemGroup[] = list.filter( isItemGroup );

By using a type guard, typescript can understand the meaning of list.filter and your error goes away.

Playground Link

0
falinsky On

You can assert that your filtered resulting array is of type ItemGroup[] using type assertion:

const groups: ItemGroup[] = list.filter( l => 'name' in l ) as ItemGroup[]