我有一个泛型类型,它只是将一个额外的属性添加到另一个类型:

type ObjWithPath<T> = { [P in keyof T]: T[P] } & { _path: string }

我有一个黑盒JS函数,需要为其提供返回类型,该函数接受此泛型类型的数组作为其参数:

declare function blackBox(objs: ObjWithPath<any>[]): any; // <-- I'd like to create a return type given the array passed in

我知道返回类型应该具有什么 struct ,但我正在努力了解这是否可能,因为似乎没有办法访问各个数组元素的泛型参数并使用它们来创建新类型.

给定具有以下类型的输入数组

const startingArray: ObjWithPath<any>[] = [
    { _path: "hello", prompt: "World" }, // ObjWithPath<{prompt: string}>
    { _path: "foo", bar: 4 },            // ObjWithPath<{bar: number}>
    { _path: "testing", value: 123 },    // ObjWithPath<{value: number}>
]

该函数的返回类型应为

{
  hello: { prompt: string },
  foo: { bar: number },
  testing: { value: number },
}

目标是使用返回的对象,我应该能够做到以下几点:

const returnObj = blackBox(startingArray)
console.log(returnObj.hello) // { prompt: "World" }
// or
console.log(returnObj["hello"]) // { prompt: "World" }

换句话说,一个对象,其中keys是每个数组项的_path个属性的值,并且值具有该数组项的泛型参数的类型.

我在这个要求的两个部分上都在努力.我不确定是否可以使用映射类型语法将数组中对象的值映射到另一个对象的键,或者是否有其他机制允许这样做?我也不知道如何实际获取单个数组项的泛型参数类型.这种类型的信息是否可用?

推荐答案

如果您希望编译器跟踪startingArray_path个属性中的字符串literal types,以便它们可以用作从blackBox()返回的值的键,以及每个属性的类型,那么您不能将其annotateObjWithPath<any>[],甚至不能像[ObjWithPath<{prompt: string}>, ObjWithPath<{bar: number}>, ObjWithPath<{value: number}>]那样使用tuple.用(非union)类型注释变量会有效地丢弃来自其初始值设定项的任何更具体的信息.

因此,在第一步中,让我们完全放弃注释,而使用const assertion来要求编译器为startingArray推断出最具体的类型:

const startingArray = [
  { _path: "hello", prompt: "World" },
  { _path: "foo", bar: 4 },
  { _path: "testing", value: 123 },
] as const; // <-- need this or the complier has no idea what strings _path are

/* const startingArray: readonly [{
    readonly _path: "hello";
    readonly prompt: "World";
}, {
    readonly _path: "foo";
    readonly bar: 4;
}, {
    readonly _path: "testing";
    readonly value: 123;
}] */

这可能比您关心的保存更多的信息,但至少现在我们可以继续进行.


鉴于此,我们希望编写一个generic实用程序类型,以从ObjWithPath<?>兼容类型的数组转换为输出对象的类型.这里有一种方法:

type ObjWithPathArrayToPlainObj<T extends readonly { _path: string }[]> =
  { [U in T[number] as U["_path"]]: Omit<U, "_path">}

该类型constrains的类型参数是具有字符串_path属性的对象数组(实际上是readonly array,它也接受可变数组).它使用key remapping将所有数组元素类型转换为键-值对.给定一个类似数组的类型T,类型T[number]是其元素类型的并集(这就是如果您index into T使用一个数字键所得到的).对于该联合中的每个元素U,我们希望使用U["_path"]作为键(同样,使用_path键索引到U),对于值,我们希望使用Omit<U, "_path">the Omit utility type来从类型中删除一个或多个键.

这是基本的方法,尽管它会产生更难读取的输出类型,因此您可以将其更改为

type ObjWithPathArrayToPlainObj<T extends readonly { _path: string }[]> =
  { [U in T[number] as U["_path"]]: 
    { [K in keyof U as K extends "_path" ? never : K]: U[K] } 
  } & {} 

它手动计算Omit的等价值(使用键重新映射,通过使用never作为键过滤出_path),并使用空对象类型计算intersecting,以说服编译器显示类似{x: {a: 1}, y: {b: 0}}而不是ObjWithPathArrayToPlainObj<[{_path: "x", a: 1}, {_path: "y", b: 0}]>的类型.


现在我们可以编写blackBox()来使用该键入,可能如下所示:

const blackBox =
  <const T extends readonly { _path: string }[]>(arr: T) =>
    Object.fromEntries(arr.map(({ _path, ...rest }) => [_path, rest])) as
    ObjWithPathArrayToPlainObj<T>;

这是一个泛型函数,它将T约束为与ObjWithPathArrayToPlainObj相同的东西.我还将其赋值为const type parameter modifier,这样,如果您为arr传递数组文字,编译器会将其视为您在创建时使用了const断言.

无论如何,类型只接受一个类型为T的数组,并返回类型为ObjWithPathArrayToPlainObj<T>的值.实际实现使用Object.fromEntries()将一组键-值对组合到一个对象,并通过将它们映射到_path属性和通过destructuring assignmentarr生成键-值对,从而生成对象的其余部分.这可能是我能想到的最简洁的编写方法,但您可以将其扩展到满足您需要的任何内容.


好的,让我们测试一下:

const resultObj = blackBox(startingArray);
/* const resultObj: {
    hello: { readonly prompt: "World"; };
    foo: { readonly bar: 4; };
    testing: { readonly value: 123; };
} */    
console.log(resultObj.hello.prompt.toUpperCase()) // "WORLD"

console.log(resultObj);
/* {
  "hello": { "prompt": "World" },
  "foo": { "bar": 4 },
  "testing": { "value": 123 }
} */

看上go 不错.类型resultObj正好对应于输出值,因此,例如,编译器知道resultObj.hello具有字符串值prompt属性.

const T修饰符使以下代码也可以与内联数组文字一起使用:

const resultObj2 = blackBox([
  { _path: "hello", prompt: "World" },
  { _path: "foo", bar: 4 },
  { _path: "testing", value: 123 },
]);
/* const resultObj2: {
    hello: { readonly prompt: "World"; };
    foo: { readonly bar: 4; };
    testing: { readonly value: 123; };
} */

如果您删除它,您将得到一些带有字符串index signature的太宽的类型,因为编译器不会跟踪_path只是string:

const blackBox = <T extends ⋯
// ------------> ^^^^ <--- removed const modifier

const resultObj2 = blackBox([
  { _path: "hello", prompt: "World" },
  { _path: "foo", bar: 4 },
  { _path: "testing", value: 123 },
]);
/* const resultObj2: {
  [x: string]: {
      prompt: string;
      bar?: undefined;
      value?: undefined;
  } | {
      bar: number;
      prompt?: undefined;
      value?: undefined;
  } | {
      value: number;
      prompt?: undefined;
      bar?: undefined;
  };
} */

Playground link to code

Typescript相关问答推荐

如何在另一个if条件中添加if条件

如何根据数据类型动态注入组件?

为什么ESLint抱怨通用对象类型?

使某些类型的字段变为可选字段'

material 表不渲染任何数据

类型,其中一条记录为字符串,其他记录为指定的

是否可以从函数类型中创建简化的类型,go 掉参数中任何看起来像回调的内容,而保留其余的内容?

记录键的泛型类型

在HighChats列时间序列图中,十字准线未按预期工作

使用动态输出类型和泛型可重用接口提取对象属性

APP_INITIALIZER在继续到其他Provider 之前未解析promise

API文件夹内的工作员getAuth()帮助器返回:{UserID:空}

Angular 16将独立组件作为对话框加载,而不进行布线或预加载

在打字应用程序中使用Zod和Reaction-Hook-Forms时显示中断打字时出错

如何在Nextjs路由处理程序中指定响应正文的类型

基于闭包类型缩小泛型类型脚本函数的范围

为什么`map()`返回类型不能维护与输入相同数量的值?

在ts中获取级联子K类型?

Typescript 编译错误:TS2339:类型string上不存在属性props

如果父级被删除或删除,如何自动删除子级?