Marcin
Marcin

Reputation: 600

Filtering array from undefined values

Can someone explain to me why in this case:

const dataValues: ValueRange[] = res.data.valueRanges.filter((range: ValueRange) => range.values);

const formattedValues: Array<SheetData | undefined> = dataValues.map(this.formatSheetRanges);

const sheetData: Array<SheetData> = formattedValues.filter((sheetData: SheetData | undefined) => sheetDataGuard(sheetData));

function sheetDataGuard(data: SheetData | undefined): data is SheetData {
  return !!(<SheetData>data).user;
}

The sheetData array will still complain that the type of it is

Array<SheetData | undefined>

but if i change the last filtering to:

const sheetData: Array<SheetData> = formattedValues.filter(sheetDataGuard);

the typescript doesn't complain anymore?

Upvotes: 3

Views: 952

Answers (1)

jcalz
jcalz

Reputation: 327819

It's because the typings for the method Array<T>.filter() in the standard TypeScript lirbary have an overload signature that specifically narrows the returned array element type if the callback function is known to be a user-defined type guard function:

 interface Array<T> {
   filter<S extends T>(
     callbackfn: (value: T, index: number, array: T[]) => value is S, 
     thisArg?: any
   ): S[];
 }

Since the type of sheetDataGuard is assignable to (value: SheetData | undefined, index: number, array: Array<SheetData | undefined>) => value is SheetData, then calling filter on an Array<SheetData | undefined> with sheetDataGuard as the callbackfn parameter will cause the compiler to select that overload with SheetData inferred for S.

But when you call it with (sheetData: SheetData | undefined) => sheetDataGuard(sheetData) instead, the type of that function is inferred to return just boolean. That's because user-defined type guard functions do not propagate and are not inferred for you. A type like x is Y generally gets widened to boolean by the compiler as soon as you start doing things with it.

You could tell the compiler that your arrow function callback is also a type guard by using an arrow-function return type annotation, like this:

const sheetData: Array<SheetData> = formattedValues.filter(
    (sheetData: SheetData | undefined): sheetData is SheetData =>
        sheetDataGuard(sheetData)
);

And the compiler should be happy. Of course if you're going to do this, you might as well forget about defining sheetDataGuard as a separate function:

const sheetData: Array<SheetData> = formattedValues.filter(
(sheetData: SheetData | undefined): sheetData is SheetData =>
    !!sheetData
);

Anyway, hope that helps. Good luck!

Upvotes: 6

Related Questions