Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript and Array.filter() working together

Tags:

typescript

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)
like image 430
extraxt Avatar asked Sep 07 '26 16:09

extraxt


1 Answers

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);

At that point, you may as well abstract checking the sub-property with type safety.

function subPropFilter<T>(prop: keyof T) {
    return (obj: T): obj is RequiredSubProperty<T, typeof prop> => {
        return obj[prop] !== undefined;
    }
}

const gpsLocationList = allHouses.
    .filter(subPropFilter("location"))
    .map(house => house.location)

Then why not abstract the entire process adding mapping to that sub-property?

function getNonNullSubProps<T, Prop extends keyof T>(arr: T[], prop: Prop) {
    return arr.filter(subPropFilter(prop)).map(obj => obj[prop] as typeof obj[Prop]) 
}

const gpsLocationList = getNonNullSubProps(allHouses, "location");

See live example that shows how awesome the suggestions are as you're using these utilities.

like image 125
Juan Mendes Avatar answered Sep 13 '26 04:09

Juan Mendes



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!