我有一个提取嵌套 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[] }
,鱼 { 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[], ...}
)...这就是函数实际返回的内容。
完整可重现示例
这对实际问题没有任何帮助,但有些人可能会发现查看其他(不相关的)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,一种将键映射到节点类型的类对象类型。 (
object
类型的约束并不是非常重要;您可以将其放松到
{}
或将其保留为
{[k: string]: any}
,甚至只使用
unknown
或不对其进行约束。
object
type 主要用于语义:它是一个键值映射,并不意味着是一个原始类型。)对于您的示例,这看起来像
{ cats: CatType, dogs: DogType }
。那么
data
参数的类型是
T
中属性的 映射类型;对于
T
中具有
K
类型键和索引访问类型
T[K]
值的每个属性,
data
的
T
键中的相应属性,
data
的相应属性键
K
具有类型
{ edges: { node: T[K] }[] }
的值。这里的目的是,当您调用
extractNodesFromData(data)
时,TypeScript 应该能够从
T
的类型推断data
。这是有效的,因为映射类型是同态(请参阅“同态映射类型”是什么意思?),并且 TypeScript 设置为能够从同态映射类型进行此类推断。如果
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
的实现,您最终需要使用类型断言或类似的东西来放松类型检查并使编译器不会抱怨。 除了非常基本的索引之外,TypeScript 类型检查器确实无法对泛型类型操作进行太多验证。它当然不知道
方法的输出如何与该映射类型相关;请参阅使用 Object.entries 时保留类型。并且它无法看到
reduce()
方法如何从空对象构建到预期类型的值。有关类似问题,请参阅在 Typescript 元组上键入归约。这不是编译器可以遵循的东西。 因此,最简单的方法是用类型断言来填充它,直到它停止报告错误,例如:
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()
的正确形状;
any
类型实际上是类型检查的逃生口。 由于编译器并没有真正为您检查实现,这意味着您需要特别小心,确保它按预期工作。一项快速检查是查看
extracted
在运行时的行为是否适合 TypeScript 为其推断的类型:
console.log(extracted.cats.map(c => c.name)) // [LOG]: ["c1", "c2"]
console.log(extracted.dogs.map(d => d.dogBreed)) // [LOG]: ["d1", "d2", "d3"]
第一眼看上去不错。但像这样的代码的最佳实践是将其应用于广泛的用例并确保其行为可接受。