我经常发现自己处于这样的情况:我有一个对象数组,我想将其转换为对象字典。这些物体可能具有某种已知的形状,但它们仍然因物体而异。我需要的是生成的对象/字典:
- 了解字典中设置的离散属性的类型
- 了解每个数据库表列/属性的详细信息
一个典型的用例是数据库的表。在这种情况下,字典将代表每个表的 API,因此它可能会共享一些方法,例如 select
, update
等等,但它所操作的属性会因型号而异。
在本例中,我们假设接口(interface)为 ITableDefinition<T>
作为每个表的 API 的一般定义,但通用是基础表带来的细节。现在假设我用以下 Database()
包裹所有表格功能:
import { ITableDefinition } from "./Table";
type IDatabase<T extends { [P in keyof T]: T[P] } = any> = {
[P in keyof T]: T[P];
};
function Database(...tables: ITableDefinition<any>[]) {
return {
tables: arrayToObject(tables),
tableNames,
};
}
// provided by @jcalz
function arrayToObject<T extends { name: S }, S extends PropertyKey>(
/** an array of objects */
arr: readonly T[]
) {
return arr.reduce(
(acc, v) => ({ ...acc, [v.name]: v }),
{} as { [V in T as V["name"]]: V }
);
}
界面ITableDefinition
上面的例子是:
import * as t from "io-ts";
export interface ITableDefinition<T extends object> {
name: Readonly<string>;
is: t.Mixed["is"];
encode: t.Mixed["encode"];
decode: t.Mixed["decode"];
select: (cols: keyof T) => string;
update: (record: Partial<T>) => string;
}
如您所见,tableNames
成功返回为字符串数组,因为每个表都有一个“name”属性,该属性始终是一个字符串。然而遗憾的是,tables
是联合类型而不是可区分联合。
Note: this much progress was only achieved with the help of @jcalz invaluable contribution of the
arrayToObject
function.
当我们使用带有静态类型对象的测试用例时,可以避免这个问题,我们可以使用 as const
TS 关键字。这将将其带回受歧视的联合,但在我们的表示例中这是不可能的。
重要:
@jcalz 提供的答案和见解对于达成解决方案至关重要,但它本身并没有解决前面提到的 Table()
的全部问题。函数正在生成对象,因此为什么要使用 TS 的 as const
不是一个选择。
为了使社区受益,我发布了一个完全有效的解决方案,但大部分繁重的工作/智能都来自@jcalz。如果将来有人有更优雅的解决方案,我会很乐意选择你的而不是我的。
最佳答案
您肯定需要使用type assertions或类似的东西来说服编译器 reduce()
将返回您想要的类型的值,因为编译器无法自动遵循“将 name
属性从数组的每个元素复制到键”的概念。
至于您想要哪种类型,有一种可能性:
function arrayToObj<
T extends { name: S }, S extends PropertyKey
>(arr: readonly T[]) {
return arr.reduce(
(acc, v) => ({ ...acc, [v.name]: v }),
{} as { [V in T as V["name"]]: V }
);
}
我们是说arr
输入arrayToObj()
类型为readonly T[]
对于某些通用类型 T
有一把类似 key 的 name
属性。
在完美的世界中,你可以直接说 T extends {name: PropertyKey}
并完成它,但不幸的是,这通常会导致编译器推断出像 string
这样的宽类型。对于name
调用电话arrayToObj()
时的属性(property)。但这对你不起作用;你需要实际的literal键。所以我引入了一个新的类型参数S extends PropertyKey
并将其用作 name
属性(property)。这个看似无关的参数旨在向编译器提示我们需要文字 name
如果可能的话。是的,这是黑魔法。请参阅microsoft/TypeScript#30680功能请求允许以不太神秘的方式进行此类提示。
无论如何,输出类型是{ [V in T as V["name"]]: V }
。这是使用key remapping in mapped types正如 TypeScript 4.1 中所介绍的,“对于 V
数组中元素值 T
的完整联合中的每个元素值 arr
,我们需要一个 V["name"]
类型的 key 。
让我们尝试一下:
const obj = arrayToObj([
{ name: "foo", val: 123 },
{ name: "bar", val: "bar" },
{ name: "baz", val: new Date() }
]);
/* const obj: {
foo: {
name: "foo";
val: number;
};
bar: {
name: "bar";
val: string;
};
baz: {
name: "baz";
val: Date;
};
} */
console.log(obj.foo.val.toFixed(2)); // 123.00
console.log(obj.bar.val.toUpperCase()); // BAR
console.log(obj.baz.val.getFullYear()); // 2021
看起来不错。实现和编译器就 obj
的类型达成一致。 .
关于typescript - 使用reduce从对象数组创建强类型TS字典,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66093590/