在类型中使用元组而不是联合数组

Pat*_*rts 2 reflection types transpose tuples typescript

有没有一种方法可以更严格地输入以下两个函数toCsv()toArray()例如typeof csv

[["key", "life", "goodbye"], ...[string, number, boolean][]]
Run Code Online (Sandbox Code Playgroud)

代替

[("key" | "life" | "goodbye")[], ...(string | number | boolean)[][]]
Run Code Online (Sandbox Code Playgroud)

typeof original相同typeof values,即

{ key: string, life: number, goodbye: boolean }[]
Run Code Online (Sandbox Code Playgroud)

代替

{ key: any, life: any, goodbye: any }[]
Run Code Online (Sandbox Code Playgroud)

我意识到不能保证{ key: 'value', life: 42, goodbye: false }使用的迭代顺序for...in,我对此很满意。即使 TypeScript 编译器不会生成与运行时相同的顺序,任何将键与每行的相应值对齐的一致顺序都是可以接受的,因为使用不依赖于任何特定的顺序。

type Key<T> = Extract<keyof T, string>;
type Column<T> = [Key<T>, ...T[Key<T>][]];
type Columns<T> = [Key<T>[], ...T[Key<T>][][]];

function toCsv<T> (array: T[]): Columns<T> {
    const columns: Column<T>[] = [];

    for (const key in array[0]) {
        columns.push([key, ...array.map(value => value[key])]);
    }

    const keys: Key<T>[] = [];
    const rows: T[Key<T>][][] = array.map(() => []);

    for (const [key, ...values] of columns) {
        keys.push(key);

        for (const [index, row] of rows.entries()) {
            row.push(values[index]);
        }
    }

    return [keys, ...rows];
}

function toArray<T> (csv: Columns<T>): T[] {
    const [keys, ...rows] = csv;

    return rows.map(
        row => keys.reduce(
            (o, key, index) => Object.assign(o, { [key]: row[index] }),
            {} as Partial<T>
        ) as T
    );
}

const values = [{ key: 'value', life: 42, goodbye: false }];
const csv = toCsv(values);
const original = toArray(csv);
Run Code Online (Sandbox Code Playgroud)

jca*_*alz 5

我不会尝试走输出特定元组排序的路线。正如您已经指出的,实际结果可能不是按该顺序排列的,因此将其呈现为这样的类型会产生误导。对编译器撒谎有时是必要的或有用的,但在这种情况下,我没有看到重大好处。

此外,即使我想这样做,让编译器将一个像联合体变成keyof T一个有序元组实际上并不容易。该类型与;"a"|"b"完全相同。"b"|"a"编译器很可能会在不让您知道的情况下使用其中之一或两者,因此您所做的任何产生["a", "b"]vs["b", "a"]的事情都可能会在您不期望的时候发生切换。您可以滥用类型系统来实现这一点,但它确实很混乱且脆弱,我建议不要这样做。


如果您确实想使用元组,则可以通过将像这样的并集转换"a"|"b"为所有可能的元组的并集来避免排序问题["a", "b"] | ["b", "a"]。这实际上在类型系统中更容易表示,因为它在联合成员上是对称的,但仍然很混乱,因为一旦拥有相当数量的属性,联合中的元素数量就变得难以管理(是的阶乘)。这样做的好处是,您对输出类型尽可能诚实。这是实现它的一种方法:

type UnionToAllPossibleTuples<T, U = T> = [T] extends [never]
    ? []
    : T extends unknown ? [T, ...UnionToAllPossibleTuples<Exclude<U, T>>] : never;

type MergedColumns<T> = UnionToAllPossibleTuples<
  { [K in keyof T]: { key: K; val: T[K] } }[keyof T]
>;

type Lookup<T, K> = K extends keyof T ? T[K] : never;

type UnmergeColumns<T> = T extends any
  ? [
      { [K in keyof T]: Lookup<T[K], "key"> },
      ...{ [K in keyof T]: Lookup<T[K], "val"> }[]
    ]
  : never;

type Columns<T> = UnmergeColumns<MergedColumns<T>>;
Run Code Online (Sandbox Code Playgroud)

您可以验证这是否有效:

interface TestType {
  key: string;
  life: number;
  goodbye: boolean;
}

type ColumnsTestType = Columns<TestType>;
// type ColumnsTestType =
// | [["key", "life", "goodbye"], ...[string, number, boolean][]]
// | [["key", "goodbye", "life"], ...[string, boolean, number][]]
// | [["life", "key", "goodbye"], ...[number, string, boolean][]]
// | [["life", "goodbye", "key"], ...[number, boolean, string][]]
// | [["goodbye", "key", "life"], ...[boolean, string, number][]]
// | [["goodbye", "life", "key"], ...[boolean, number, string][]]
Run Code Online (Sandbox Code Playgroud)

这很有趣,但可能仍然太脆弱和混乱,不适合我推荐。


备份起来,似乎您真正关心的是保留和T之间的类型,并且原始数组类型虽然准确,但却是有损的。在这种情况下,对您的原始代码进行微小的更改怎么样?toCsv()toArray()

type Columns<T> = [Key<T>[], ...T[Key<T>][][]] & { __original?: T };
Run Code Online (Sandbox Code Playgroud)

这里,Columns<T>本质上与之前的类型相同,但有一个original以 type命名的可选额外属性T。该属性永远不会在运行时实际存在或使用。是的,你可能在这里欺骗了编译器,但实际上并没有撒谎;出来的东西toCsv()将没有__original属性,这确实匹配{__original?: T}。不过,这种欺骗很有用,因为它为编译器提供了足够的信息来了解往返过程中发生的情况。观察:

const values = [{ key: "value", life: 42, goodbye: false }];
const csv = toCsv(values);
// const csv: Columns<{ key: string; life: number; goodbye: boolean; }>
const original = toArray(csv); 
// const original: { key: string; life: number; goodbye: boolean; }[]
Run Code Online (Sandbox Code Playgroud)

这对我来说看起来不错,也是我推荐的。


回顾一下:如果您想对编译器撒谎,请不要对元组顺序撒谎。说实话元组顺序太乱了。相反,对可选财产撒一个小谎。

好的,希望有帮助。祝你好运!

链接到代码

  • @WongJiaHau你的答案很好,但是你改变了程序的运行时行为,我认为这不适用于OP。 (2认同)