Filter an array in Typescript

Last updated October 1, 2021 by Jacob Paris

I had a typed array and wanted to remove elements with an empty value. In vanilla Javascript that's easy enough, but instead of regular strings I was using string unions to restrict the available options.

1type Property = {
2 measurements: Measurement[]
3}
4
5type Measurement = {
6 type: MeasurementType
7 value: number
8 unit: MeasurementUnit
9}
10
11type MeasurementType =
12 | 'LOT_DEPTH'
13 | 'LOT_AREA'
14 | 'LIVING_AREA'
15
16type MeasurementUnit =
17 | 'SQUARE_FEET'
18 | 'SQUARE_METERS'
19 | 'FEET'
20 | 'METERS'
21
22function getProperty(input): Property {
23 return {
24 measurements: [
25 {
26 type: 'LOT_DEPTH',
27 value: input.lotDepth.value,
28 unit: input.lotDepth.unit,
29 },
30 {
31 type: 'LOT_AREA',
32 value: input.lotArea.value,
33 unit: input.lotArea.unit,
34 },
35 {
36 type: 'LIVING_AREA',
37 value: input.livingArea.value,
38 unit: input.livingArea.unit,
39 },
40 ].filter((measurement) => measurement.value),
41 }
42}
43

Typescript was able to correctly infer the type of the measurements array, but as soon as I added the filter it broke. Why? Filter returns a regular array of regular objects, and the type information about the string unions was lost

Type 'string' is not assignable to type 'MeasurementName'

Solutions

The solution I ended up going with was to use type predicates to tell filter which type to use. I found out afterward that TS can infer correctly without the type predicate as long as it's split with an intermediate variable.

Type predicates

.filter((measurement): measurement is Measurement => measurement.value)

Type predicates (JSDoc)

.filter(/** @type function(*): measurement is Measurement */ (measurement) => measurement.value)

Intermediate variable (JSDoc)

function getProperty(input): Property {
/** @type {Measurement[]} */
const measurements = [
{
type: 'LOT_DEPTH',
value: input.lotDepth.value,
unit: input.lotDepth.unit,
},
{
type: 'LOT_AREA',
value: input.lotArea.value,
unit: input.lotArea.unit,
},
{
type: 'LIVING_AREA',
value: input.livingArea.value,
unit: input.livingArea.unit,
},
]
return {
measurements: measurements.filter(
(measurement) => measurement.value,
),
}
}