【问题标题】:Typescript and Array.filter() working togetherTypescript 和 Array.filter() 一起工作
【发布时间】:2021-10-20 18:54:42
【问题描述】:

当使用Array.filter() 时,我不确定如何实现我在下面描述的内容。

我不想仅仅为此创建一个新类型(但如果没有其他办法,那也没关系):

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)

【问题讨论】:

标签: typescript


【解决方案1】:

使您的过滤器回调为type guard,强制位置不是可选的。

我知道有一个现有的帖子with a great answer,但由于要推断这种情况的解决方案并不容易(映射到可选的子属性),而且过去几天我不得不做同样的事情,我会在这里分享。

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

为了清楚起见,我更愿意将其拆分为单独的声明。

type HouseLocationRequired = House & {location: GPSLocation};

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

如果你想真正减少类型的重复,你可以使用Required<T>Pick<>

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

并将其进一步抽象一层:

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

此时,您不妨抽象检查具有类型安全性的子属性。

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)

那为什么不抽象出整个过程来添加映射到那个子属性呢?

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

请参阅live example,它显示了您在使用这些实用程序时的建议是多么棒。

【讨论】:

    【解决方案2】:

    确实烦人。

    这里有两种常见的方法。

    (1)使用as强制编译器将其视为GPSLocation

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

    (2) 使用一个如果输入为假则抛出错误的函数:

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

    更新

    其实还有第三种方法:使用flatMap()

    const gpsLocationList: string[] = allHouses.flatMap(
      house => house.location !== undefined ? [house.location] : [])
    

    这实际上效果很好,并且 100% 安全。缺点是:

    • .flatMap() 并非在所有平台上都可用。您需要在 tsconfig.json 文件中使用 target: "es2019"
    • 可读性不强。

    【讨论】:

    • 有趣的方法。只是铸造感觉像是在降低成本,但我喜欢“保证其输入不为空的函数”守卫。
    猜你喜欢
    • 2022-12-01
    • 2016-08-31
    • 1970-01-01
    • 2019-07-23
    • 2020-04-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多