如何定义一个TypeScript函数,该函数基于筛选器从对象返回属性的子集?

guicsvcw  于 2023-03-04  发布在  TypeScript
关注(0)|答案(2)|浏览(125)

考虑下面的代码。

interface IEmployee {
    ID: string
    Employee: string
    Phone: number
    Town: string
    Email: string
    Name: string
}

// Should have all IEmployee properties 
let allProps = findOne<IEmployee>("ID = 1234234")

// Should be of type Pick<IEmployee, "Email"| "Phone">  
let picked = findOne<IEmployee>("ID = 1234234", ["Email", "Phone"])

如何声明findOne?是否有可能实现,保持与上面相同的调用代码?

可能的解决方案

使用数组(首选,不工作)

下面的实现不起作用,因为typeof fields[number]将是Array<keyof T>

function findOne<T>(filter: string, fields: Array<keyof T> =[]): Pick<T,typeof fields[number]> {
    return db.select(filter, fields)
}

添加第二个泛型参数(Works)

这是可行的,但不容易阅读

function select<T, K extends Partial<T> = T>(filter: string, fields = Array<keyof K>): K {
    return db.select(filter, fields)
}

const props = ["Email", "Phone"] as const
type propsType = Pick<IEmployee, typeof props[number]>

let allProps = select<IEmployee>("Employee: 1234234")
let picked = select<IEmployee, propsType>("Employee: 1234234")
nlejzf6q

nlejzf6q1#

要使其正常工作,需要在两个类型参数中将findOne()设置为genericT对应于您要查找的基对象类型,K对应于您要选取的T中的键的并集。

declare const findOne: <T, K extends keyof T>(
  query: string, keys?: K[]
) => { [P in K]: T[P] }

但是有一个问题。你想在调用findOne()时手动指定T,并且你还想让编译器从keys参数推断K,而不需要你指定它。不幸的是,Typescript不支持这种类型的部分类型参数推断。或者编译器可以尝试推断所有的类型参数(注意,给K一个默认类型参数并不能使能部分类型参数推断;你不需要手动指定一个参数的默认值,但是编译器仍然不会为你推断它;它将退回到默认值)。
microsoft/TypeScript#26242上有一个长期的开放特性请求,要求进行部分类型参数推断。

// NOT VALID TS, DON'T TRY THIS
declare const findOne: <T, K extends keyof T = infer>(
  query: string, keys?: K[]
) => { [P in K]: T[P] }

但是,除非这是曾经实现,你将需要围绕它工作。
在这种情况下,我更喜欢的解决方法是curry函数;也就是说,把一个有两个类型参数的函数拆分成一个有一个类型参数的函数,这个函数返回一个有另一个类型参数的函数,就像这样:

declare const findOne: <T>() => <K extends keyof T>(
  query: string, keys?: K[]
) => { [P in K]: T[P] }

现在你可以调用findOne并手动指定<T>,当你调用返回的函数时,K将被推断出来:

let allProps: IEmployee = findOne<IEmployee>()("ID = 1234234")
let picked = findOne<IEmployee>()("ID = 1234234", ["Email", "Phone"])
// let picked: { Phone: number;  Email: string; }

在这里有额外的函数调用有点奇怪,但是它起作用了!一旦你开始使用curry函数,你会发现存储中间结果和重用它更容易:

const findOneIEmployee = findOne<IEmployee>();
allProps = findOneIEmployee("ID = 1234234");
picked = findOneIEmployee("ID = 1234234", ["Email", "Phone"]);

Playground代码链接

v8wbuo2f

v8wbuo2f2#

我不确定这是否是您要找的,但这里有一个findOne的变体。

interface Dog {
  name: string
  age: number
  breed: string
  id: number
}

const dogs: Dog[] = [
  {
    id: 1,
    name: 'Rex',
    age: 3,
    breed: 'German Shepherd'
  },
  {
    id: 2,
    name: 'Fido',
    age: 4,
    breed: 'Golden Retriever'
  },
  {
    id: 3,
    name: 'Spot',
    age: 5,
    breed: 'Poodle'
  }
]

function findOne<DataType, KeyType extends keyof DataType>(
  filter: [KeyType, keyof KeyType],
  items: DataType[],
  keys: KeyType[]
): Partial<DataType> | undefined {
  for (const item of items) {
    if (item[filter[0]] === filter[1]) {
      return {
        ...keys.reduce((acc, key) => {
          return {
            ...acc,
            [key]: item[key]
          }
        }, {})
      }
    }
  }
}

const dog = findOne(['age', 5], dogs, ['id', 'age'])

returns {
   id: 3, age: 5
}

为了让一切更清楚,我们可以将filter作为一个对象:

function findOne<DataType, KeyType extends keyof DataType>(
  filter: {
    key: KeyType
    value: keyof KeyType
  },
  items: DataType[],
  keys: KeyType[]
): Partial<DataType> | undefined {
  for (const item of items) {
    if (item[filter.key] === filter.value) {
      return {
        ...keys.reduce((acc, key) => {
          return {
            ...acc,
            [key]: item[key]
          }
        }, {})
      }
    }
  }
}

const dog = findOne(
  {
    key: 'id',
    value: 2
  },
  dogs,
  ['id', 'age']
)

相关问题