Search code examples
typescripttypestypescript-typingsunion-types

Typescript: map union type to a single type


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?


Solution

  • 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