我有一个用于提取嵌套GraphQL数据的函数:

const data = {
   cats: { edges: [ {node: cat1}, {node: cat2} ] },
   dogs: { edges: [ {node: dog1}, {node: dog2}, {node: dog3} ] },
}

const extracted = extract<{ cats: CatType, dogs: DogType }>(data);

// extracted = {
//   cats: [cat1, cat2], // CatType[]
//   dogs: [dog1, dog2, dog3], // DogType[]
// }

然而,我很难正确地获得我的类型签名. 我想说的是"这个函数接受具有字符串键和类型值的接口参数,并返回具有相同键和相应类型数组值的对象".

换句话说,如果函数得到{ cats: nestedCats },它应该返回{ cats: CatType[] }(即.cats: T['cat'])).如果它得到了{ dogs: nestedDogs },它应该返回{ dogs: DogType[] }、fish { fishes: FishType[] }等等.如果它得到了多个类型,它应该返回一个包含所有类型的数组的对象.

现在我有一个签名:

export const extractNodesFromData = <T extends { [key: string]: any }> =>

然而,我有两个问题:

  • A)在我的函数中,当我迭代数据时,如何引用该数据的类型? 例如:

    Object.entries(data).map(([key, value]) => // How do I type value?

    Object.entries(data).map((([key, value: T[key]]) => // Doesn't work

  • B)我无法指定函数的返回类型:我不知道如何将我提供的类型({ cat: CatType, ...}个)转换为相同的类型,但对于数组(即. { cat: CatType[], ...}).这就是该函数实际返回的内容.

Full Reproducible Example

这对实际问题没有任何增加,但有些人可能会发现看到其他(不相关)JS/TS很有帮助:

interface CatType {
   id: string;
   name: string;
   catBreed: string;
}
interface DogType {
   id: string;
   name: string
   dogBreed: string;
}

const extractNodes = <T>(data): T[] =>
    data?.edges?.map(({ node }) => node);

const extractNodesFromData = <T extends { [key: string]: any }> (data: any) => 
Object.entries(data).reduce(
    (extracted, [key, value: T[key]]) => ({
      ...extracted,
      [key]: extractNodes<T[key]>(value)
    }),
    {}
  )

推荐答案

我会给extractNodesFromData个以下呼叫签名:

declare const extractNodesFromData: <T extends object>(
  data: { [K in keyof T]: { edges: { node: T[K] }[] } }
) => { [K in keyof T]: T[K][] }

该函数是T中的generic,这是一种类似对象的类型,将键映射到 node 类型. (the object type中的constraint并不特别重要;您可以将其放宽到{}或保留为{[k: string]: any},甚至仅使用unknown或不受限制. object类型主要只是用于语义:它是一个关键-值映射,并不意味着是原始类型.)对于您的示例,看起来像{ cats: CatType, dogs: DogType }.

那么data参数的类型是T中的属性的mapped type;对于T中的每个属性,具有类型K的键和值indexed access type T[K]data个键中的相应属性为T,键K处的相应属性data具有类型{ edges: { node: T[K] }[] }的值.这里的目的是,当您调用extractNodesFromData(data)时,TypScript应该能够从data的类型调用infer T.这是有效的,因为映射类型是homomorphic(请参阅What does "homomorphic mapped type" mean?),并且TypScript被设置为能够从同形映射类型做出此类推断. 如果data是类型{ cats: { edges: { node: CatType }[] }, dogs: { edges: { node: DogType }[] } },那么T应该被推断为类型{ cats: CatType, dog: DogType }

返回类型是形式{ [K in keyof T]: T[K][] }的映射类型,因此T的每个属性(键为类型K和值为类型T[K])都成为输出的属性(键为K和值为T[K][]);本质上用数组类型包装所有属性. 因此,如果T{ cats: CatType, dog: DogType },那么返回类型是{ cats: CatType[], dog: DogType[] }

让我们确保这适用于一个示例:

const cat1: CatType = { id: "c1", name: "c1", catBreed: "c1" };
const cat2: CatType = { id: "c2", name: "c2", catBreed: "c2" };
const dog1: DogType = { id: "d1", name: "d1", dogBreed: "d1" };
const dog2: DogType = { id: "d2", name: "d2", dogBreed: "d2" };
const dog3: DogType = { id: "d3", name: "d3", dogBreed: "d3" };
const data = {
  cats: { edges: [{ node: cat1 }, { node: cat2 }] },
  dogs: { edges: [{ node: dog1 }, { node: dog2 }, { node: dog3 }] },
}

const extracted = extractNodesFromData(data);
// const extracted: { cats: CatType[], dogs: DogType[] }

看起来不错


现在,对于extractNodesFromData的实现,您最终需要使用type assertions或类似内容来放松类型判断并让编译器不要抱怨.

除了非常基本的索引之外,TypScript类型判断器确实无法对通用类型操作进行太多验证.它当然不知道the Object.entries() method的输出如何与映射类型相关;请参阅Preserve Type when using Object.entries. 而且它无法看到the reduce() method将如何从空对象构建为预期类型的值.有关类似问题,请参阅Typing a reduce over a Typescript tuple.这不是编译器可以遵循的内容.

因此,最简单的方法是在它中添加类型断言,直到它停止报告错误,例如:

const extractNodesFromData = <T extends object>(
  data: { [K in keyof T]: { edges: { node: T[K] }[] } }) =>
  Object.entries(data).reduce(
    (extracted, [key, value]) => ({
      ...extracted,
      [key]: extractNodes(value as any)
    }),
    {}
  ) as { [K in keyof T]: T[K][] }

我们只需断言返回类型,甚至在其中抛出value as any断言,以使编译器不必验证value是否是extractNodes()的正确形状; the any type实际上是类型判断的换出舱口.

由于编译器并没有真正为您判断实现,这意味着您需要特别小心,以确保它按预期工作. 一个快速判断是查看extracted在运行时的行为是否适合为其推断的TypScript类型:

console.log(extracted.cats.map(c => c.name)) // [LOG]: ["c1", "c2"] 
console.log(extracted.dogs.map(d => d.dogBreed)) // [LOG]: ["d1", "d2", "d3"] 

乍一看不错.但此类代码的最佳实践是将其置于广泛的用例中,并确保其行为可接受.

Playground link to code

Typescript相关问答推荐

您可以创建一个类型来表示打字员吗

对于使用另一个对象键和值类型的对象使用TS Generics

在TypeScript中,除了映射类型之外还使用的`in`二进制运算符?

具有泛型类方法的类方法修饰符

未在DIST中正确编译TypeScrip src模块导入

对扩展某种类型的属性子集进行操作的函数

通过字符串索引访问对象';S的值时出现文字脚本错误

如何消除在Next.js中使用Reaction上下文的延迟?

已解决:如何使用值类型限制泛型键?

是否将自动导入样式从`~/目录/文件`改为`@/目录/文件`?

刷新页面时,TypeScrip Redux丢失状态

常量类型参数回退类型

有没有更干净的方法来更新**key of T**的值?

完全在类型系统中构建的东西意味着什么?

是否可以将类型参数约束为不具有属性?

T的typeof键的Typescript定义

两个接口的TypeScript并集,其中一个是可选的

TS 排除带点符号的键

Typescript 子类继承的方法允许参数中未定义的键

React-Router V6 中的递归好友路由