Используйте Tuple вместо Array of Union в Type - PullRequest
1 голос
/ 02 октября 2019

Можно ли более строго ввести следующие две функции toCsv() и toArray(), чтобы typeof csv было

[["key", "life", "goodbye"], ...[string, number, boolean][]]

вместо

[("key" | "life" | "goodbye")[], ...(string | number | boolean)[][]]

и typeof original совпадает с typeof values, то есть

{ key: string, life: number, goodbye: boolean }[]

вместо

{ key: any, life: any, goodbye: any }[]

Я понимаю, что порядок итерации { 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);

Ответы [ 2 ]

2 голосов
/ 02 октября 2019

Я бы не стал идти по пути вывода определенного порядка кортежей. Как вы уже заметили, фактический результат может быть не в таком порядке, поэтому было бы неверно представлять его как такой тип. Ложь компилятору иногда необходима или полезна, но в этом случае я не вижу существенного преимущества.

Более того, даже если бы я хотел сделать это, на самом деле не так-то просто заставить компилятор повернутьобъединение типа keyof T в упорядоченный кортеж. Тип "a"|"b" точно такой же, как и "b"|"a";компилятор может очень хорошо использовать один или другой или оба, не сообщая вам об этом, и поэтому все, что вы делаете, что производит ["a", "b"] против ["b", "a"], может переключиться, когда вы этого не ожидаете. Вы можете злоупотребить системой типов , чтобы это произошло, но она действительно грязная и хрупкая, и я рекомендую против этого.


Если вы действительно хотите использовать кортежи, вы можете избежатьупорядочить проблему, превратив объединение типа "a"|"b" в объединение всевозможных кортежей типа ["a", "b"] | ["b", "a"]. На самом деле это немного проще представить в системе типов, потому что она симметрична по отношению к членам объединения, но все еще грязна, потому что естественным способом представления этого преобразования является тип, который компилятор рассматривает как циклический, и потому что, как только вы получите приличныйчисло свойств число элементов в объединении становится неуправляемым (yay факториал). Преимущество в том, что вы действительно и действительно честны с типом вывода. Вот один из способов его реализации (для кортежей длиной до 7 ... вы можете увеличить его, если вам нужно)

type UnionToAllPossibleTuples<T> = UTAPT<T>;
type UTAPT<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT1<Exclude<U, T>>> : never;
type UTAPT1<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT2<Exclude<U, T>>> : never;
type UTAPT2<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT3<Exclude<U, T>>> : never;
type UTAPT3<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT4<Exclude<U, T>>> : never;
type UTAPT4<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT5<Exclude<U, T>>> : never;
type UTAPT5<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT6<Exclude<U, T>>> : never;
type UTAPT6<T, U = T> = [T] extends [never] ? [] : T extends any ? Cons<T, UTAPT7<Exclude<U, T>>> : never;
type UTAPT7<T, U = T> = []; // bail out
type Cons<T, U = []> = U extends any[]
  ? ((t: T, ...u: U) => void) extends ((...r: infer R) => void) ? R : never
  : [T];

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>>;

И вы можете убедиться, что это работает:

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][]]

Это забавно, но, вероятно, все еще слишком хрупко и грязно, чтобы я мог порекомендовать.


Резервное копирование, похоже, что вы действительно заботитесь о сохранении типа T по toCsv() и toArray(), и что исходный тип массива, хотя и точен, был с потерями. В таком случае, как насчет этого незначительного изменения вашего исходного кода?

type Columns<T> = [Key<T>[], ...T[Key<T>][][]] & { __original?: T };

Здесь Columns<T> по сути того же типа, что и раньше, но имеет дополнительное дополнительное свойство с именем original с типом 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; }[]

Это выглядит хорошо для меня и то, что я бы порекомендовал.


РЕКАП: Если вы хотите лгать компилятору, не ври насчет порядка кортежей,Говорить правду о порядке кортежей слишком грязно. Вместо этого скажите небольшую ложь о необязательном свойстве.

Хорошо, надеюсь, это поможет. Удачи!

Ссылка на код

1 голос
/ 02 октября 2019

Мое решение немного хакерское, но оно работает. Волшебство заключается в том, что тип T передается в свойство original, так что его можно безошибочно получить обратно без указания типа keys и values.

type CSV<T> = {values: ((keyof T)[] | (T[keyof T])[])[], original: T}

const toCsv = <T extends object>(values: T[]): CSV<T> => {
  if(values.length === 0) {
    throw new Error('Values must have length of more than one')
  }
  else {
    return {
      values: [
        Object.keys(values[0]) as (keyof T)[],
        ...values.map(Object.values) as T[keyof T][][],
      ] as ((keyof T)[] | (T[keyof T])[])[],
      original: undefined
    }  
  }
}

const toArray = <T extends object>(csv: CSV<T>): T[] => {
  const keys = csv.values[0] as (keyof T)[]
  const valuess = csv.values.slice(1) as ((T[keyof T])[])[]
  return valuess.map(values => values.reduce<T>((result, value, index) => ({...result as any, [keys[index]]: value}), {} as T))
}

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

console.log(csv.values) // this will be in the required intermediate format
console.log(original)

type Result = typeof original extends typeof values ? true : never

Тип original будет таким же, как values. Вы можете проверить себя, наведя курсор на Result.

Помимо проверки типов, реализация также работает во время выполнения.

Обратите внимание, что свойство original нигде не используется в toArray функция, ее единственное назначение - просто передавать информацию о типах.

...