Я бы не стал идти по пути вывода определенного порядка кортежей. Как вы уже заметили, фактический результат может быть не в таком порядке, поэтому было бы неверно представлять его как такой тип. Ложь компилятору иногда необходима или полезна, но в этом случае я не вижу существенного преимущества.
Более того, даже если бы я хотел сделать это, на самом деле не так-то просто заставить компилятор повернутьобъединение типа 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; }[]
Это выглядит хорошо для меня и то, что я бы порекомендовал.
РЕКАП: Если вы хотите лгать компилятору, не ври насчет порядка кортежей,Говорить правду о порядке кортежей слишком грязно. Вместо этого скажите небольшую ложь о необязательном свойстве.
Хорошо, надеюсь, это поможет. Удачи!
Ссылка на код