如果您希望编译器跟踪startingArray
的_path
个属性中的字符串literal types,以便它们可以用作从blackBox()
返回的值的键,以及每个属性的类型,那么您不能将其annotate为ObjWithPath<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 assignment从arr
生成键-值对,从而生成对象的其余部分.这可能是我能想到的最简洁的编写方法,但您可以将其扩展到满足您需要的任何内容.
好的,让我们测试一下:
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个