在类型中使用元组而不是联合数组
Posted
技术标签:
【中文标题】在类型中使用元组而不是联合数组【英文标题】:Use Tuple instead of Array of Union within Type 【发布时间】:2020-01-31 07:52:18 【问题描述】:有没有办法更严格地键入以下两个函数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);
【问题讨论】:
只是想澄清一下,您的主要目的是当您调用result = toArray(toCsv(xs))
时,您希望result
的类型与xs
相同,对吧?
@WongJiaHau 是的,这是正确的,但我也有兴趣让toCsv(xs)
的中间类型与我要求的相同。
我在这里仍然有一些理解问题:你为什么要输入 ...(string | number | boolean)[][]
的东西 (rows
)(我想你忘记了额外的 []
层?)是类型[string, number, boolean]
而不是?这不会不利于您的实施吗?
PS:这是playground(一条评论的URL太长)
@ford04 哦,很好!感谢您跟进您的澄清请求。我会更新我的问题,但幸运的是我已经有了一个很好的解决方案。
【参考方案1】:
我不会尝试输出特定元组排序的路线。正如您已经指出的那样,实际结果可能不是该顺序,因此将其呈现为这种类型会产生误导。有时对编译器撒谎是必要的或有用的,但在这种情况下,我没有看到主要的好处。
此外,即使我想这样做,实际上也不容易让编译器将keyof T
这样的联合变成有序元组。 "a"|"b"
类型与 "b"|"a"
完全相同;编译器很可能会在不让您知道的情况下使用其中一个或另一个或两者,因此您所做的任何产生["a", "b"]
与["b", "a"]
的操作都可能在您不期望的情况下切换。你可以abuse the type system 来实现它,但它真的很乱而且很脆弱,我建议不要这样做。
如果您真的想使用元组,您可以通过将"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>>;
您可以验证此方法是否有效:
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][]]
这很有趣,但可能仍然过于脆弱和凌乱,不适合我推荐。
备份起来,您真正关心的似乎是在 toCsv()
和 toArray()
之间保留类型 T
,并且原始数组类型虽然准确,但有损。既然如此,对你原来的代码做这个小改动怎么样?
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; []
这对我来说很好,我会推荐。
回顾:如果你想对编译器撒谎,不要对元组顺序撒谎。说出元组顺序的真相太混乱了。相反,对可选属性撒个小谎。
好的,希望对您有所帮助。祝你好运!
Link to code
【讨论】:
令人难以置信的 TypeScript 一如既往的回答!根据我对a particular github issue 的阅读,我期待一个类似于“诚实但混乱”方法的解决方案,但我认为您的最终建议是最实用的,最好的部分是它不需要任何重大重构。跨度> @PatrickRoberts 这不是我给你的答案吗? @WongJiaHau 你的回答很好,但你改变了程序的运行时行为,我认为这不适用于 OP。【参考方案2】:我的解决方案有点老套,但它确实有效。神奇之处在于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
函数中的任何地方都没有使用,它的唯一目的仅仅是传递类型信息。
【讨论】:
这与我在问题中指出的结构不同。中间格式旨在被序列化,不得更改。你的values.map(value => Object.keys(value).map(key => value[key]))
也可能是values.map(Object.values)
你能提供一个中间格式的例子吗?问题中的任何地方都没有提到它。
这是我问题的最重要的部分:[["key", "life", "goodbye"], [string, number, boolean]]
那是中间格式的类型,我的意思是我要一个值的例子。
[["key", "life", "goodbye"], ["value", 42, false]]
... 我以为你可以从类型推断出来。以上是关于在类型中使用元组而不是联合数组的主要内容,如果未能解决你的问题,请参考以下文章