键入强大的实用程序以将 KV 对数组转换为原始字典

Type strong utility to convert array of KV pairs to original dictionary

我有一个函数 dictToKv 可以将字典转换为具有 keyvalue 属性的对象数组:

/** used to help get type literals versus wide types */
type Narrowable = string | number | boolean | symbol | object | undefined | void | null | {};

/** just like Object.keys() but preserves keyof relationship */
function keys<T extends {}>(obj: T) { Object.keys(obj) as unknown as Array<keyof T> };

/** Helps shape an object into an array where key and value relationship is retained */
type KeyValueFrom<T extends object> = Array<{ [K in keyof T]: { key: K; value: T[K] } }[keyof T]>;

/** convert a dictionary to an array of objects with key and value props */
function dictToKv<N extends Narrowable, T extends Record<string, N>>(obj: T): KeyValueFrom<T> {
  return keys(obj).map(k => {
    return { key: k, value: obj[k] };
  });
}

借助我的 KeyValueFrom<T> 实用程序,我可以明确说明 return 的类型并且此 return 值不可迭代但保留所有类型文字信息:

const obj = { id: 123, foo: "bar" } as const;
const kv1 = dictToKv(obj);

for(const kv of kv1) {
  if(kv.key === "id") {
    // strong types with no unions
    type cases = [
      Expect<Equal<typeof kv.key, "id">>,
      Expect<Equal<typeof kv.value, 123>>
    ]
  }
}

我遇到的问题是相反的。逆向的 运行 时间方面很简单,但困难的部分似乎是创建一个类型实用程序来执行这种逆向转换。

到目前为止我得到的是:

type DictFrom<T extends { key: string; value: unknown }[]> = 
   Record<T[number]["key"], T[number]["value"]>;

这确实正确键入了字典的键并保持了文字类型,但值现在是联合类型:

type Inverse = DictFrom<typeof kv1>;
type cases = [
  // Expect<Equal<Inverse, typeof obj>>
  Expect<Equal<Inverse, { foo: 123 | "bar"; id: 123 | "bar" }>>
];

有什么方法可以使用 Typescript 的推理来实现非联合类型吗?

Typescript Playground

这可以用 key remapping in mapped types 相当直接地完成,像这样:

type DictFrom<T extends { key: string; value: unknown }[]> = 
  { [R in T[number] as R["key"]]: R["value"] };

使用您的 KeyValueFrom 执行此操作:

interface Foo {
  id: number,
  foo: string
}

type FooKV = KeyValueFrom<Foo>;
/* type FooKV = ({
    key: "id";
    value: number;
} | {
    key: "foo";
    value: string;
})[] */

我们或多或少可以用 DictFrom:

撤销它
type FooDict = DictFrom<FooKV>
/* type FooDict = {
    id: number;
    foo: string;
} */

在您的测试中这些是否完全相等与 readonly 或可选性和其他可能导致 DictFrom<KeyValueFrom<T>>T 不同的怪癖有关.

Playground link to code