typescript - 使用reduce从对象数组创建强类型TS字典

标签 typescript

我经常发现自己处于这样的情况:我有一个对象数组,我想将其转换为对象字典。这些物体可能具有某种已知的形状,但它们仍然因物体而异。我需要的是生成的对象/字典:

  1. 了解字典中设置的离散属性的类型
  2. 了解每个数据库表列/属性的详细信息

一个典型的用例是数据库的表。在这种情况下,字典将代表每个表的 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 的类型达成一致。 .

Playground link to code

关于typescript - 使用reduce从对象数组创建强类型TS字典,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66093590/

相关文章:

typescript - 回调参数的条件类型

typescript - 仅获取从映射到数组的值

javascript - 如何在 Angular 中用逗号替换换行符

reactjs - 如何在 react 和 typescript 中将粗体标签插入模板文字字符串

javascript - 按键排序字典 - Ramda

angular - 如何在不重叠的情况下显示多个 snackbar

css - 以 Angular 显示播放/暂停按钮

testing - 带有 Browserify 和 TypeScript 模块的 Wallaby

TypeScript array.map 允许具有 interred 返回类型的附加属性

typescript - 从 Typescript 引用 DLL