Home > Net >  Typescript and Array.filter() working together
Typescript and Array.filter() working together

Time:10-21

When using Array.filter() I'm not sure how to achieve what I'm describing below.

I don't want to create a new type just for this (but if there's no other way around, that's okay):

interface GPSLocation {
  lat: number
  lng: number
}

interface House {
  address: string
  location?: GPSLocation
}

const house01: House = {
  address: '123 street'
}

const house02: House = {
  address: '123 street',
  location: {
    lat: 111.111,
    lng: 222.222
  }
}

const allHouses = [house01, house02]

// Infered by Typescript: const gpsLocationList: (GPSLocation | undefined)[]
// Expected: const gpsLocationList: (GPSLocation)[]
const gpsLocationList = allHouses.filter((house) => house.location !== undefined).map(house => house.location)

CodePudding user response:

Indeed annoying.

Here are two common approaches.

(1) Use as to force the compiler to treat it as GPSLocation:

const gpsLocationList = allHouses.filter((house) => house.location !== undefined)
  .map(house => house.location as GPSLocation)

(2) Use a function that throws an error if its input is falsy:

function reify<T>(t: T|null|undefined): T {
  if (t === null || t === undefined) {
    throw new Error(`got a falsy value`)
  }
  return t
}

const gpsLocationList = allHouses.filter((house) => house.location !== undefined)
  .map(house => reify(house.location))

CodePudding user response:

Make your filter callback be a type guard that forces location not to be optional.

I know there's an existing post with a great answer, but since it's not that easy to infer the solution for this case (mapping to an optional subproperty), and I had to do the same thing in the past few days, I will share this here.

const gpsLocationList = allHouses
    .filter((house): house is House & {location: GPSLocation} => {
         return house.location !== undefined;
    })
    .map(house => house.location);

I would prefer to split this into a separate declaration for clarity.

type HouseLocationRequired = House & {location: GPSLocation};

const gpsLocationList = allHouses
    .filter((house): house is HouseLocationRequired => {
         return house.location !== undefined;
    })
    .map(house => house.location);

If you want to really minimize duplication of types, you can use Required<T> and Pick<>

type HouseLocationRequired = House & Required<Pick<House, "location">>;

And abstract it one level further:

type RequiredSubProperty<Class, Prop extends keyof Class> = Class & Required<Pick<Class, Prop>>;

const gpsLocationList = allHouses
    .filter((house): house is RequiredSubProperty<House, "location">  => {
         return house.location !== undefined;
    })
    .map(house => house.location);

See live example

  • Related