如何声明类型的 "transposed" 版本?

How to declare a "transposed" version of a type?

我想定义一个 TS 函数来将对象转置为数组,例如:

const original  = { 
    value: [1, 2, 3], 
    label: ["one", "two", "three"] 
}

const transposed = [ 
    {value: 1, label: "one"}, 
    {value: 2, label: "two"},
    {value: 3, label: "three"}
]

然后我想声明一个接收 original 并输出 transposed 的函数,反之亦然,例如:

function tranpose(original: T): Array<T> // Incorrect, the output should be a transposed version of the object.

如何定义对象的转置版本并正确声明函数?

--

PS我问的不是实现,而是输入和声明。

您可以在对象字段的类型中使用泛型

function transpose<A,B>(original:{value:A[], label:B[]}): { value:A, label:B }[] {
    const objs:  {value:A, label:B}[] = [];
    original.value.forEach( (value, i) => {
        const label = original.label[i];
        objs.push({label, value}); 
    });
    return objs;
}

const obj = {
    value: [ 1,2,3 ],
    label: ['One', 'Two', 'Three']
};

console.log(transpose(obj));

Playground link

如果你需要一个类型来检查原始类型是否完全转置,你首先需要在原始类型上使用const assertion as const (或 valuelabel 键下可变数组的元素类型将被加宽。


有一种方法可以在不生成索引的情况下键入转置,从而产生更简洁和健壮的类型(其余逻辑与下面的实用程序中的逻辑相同):

const original = { 
    value: [1, 2, 3], 
    label: ["one", "two", "three"] 
} as const;

type T2<T extends { value: readonly number[], label: readonly string[] }> = {
    [ P in keyof Omit<T["value"], keyof readonly any[]> ] : { value: T["value"][P], label: P extends keyof T["label"] ? T["label"][P] : never }
}  & readonly any[];

type test = T2<typeof original>;

const transposed: test = [
    { value: 1, label: "one" },
    { value: 2, label: "two" },
    { value: 3, label: "three" }
];

Playground


递归版本

接下来,您必须生成足够的索引以确保生成的元组中的元素正确定位。这可以通过递归条件类型来完成,但要注意递归深度限制(在此实现中为~23):

type GenIndices<L extends number, A extends any[] = []> = Omit<A["length"] extends L ? A : GenIndices<L, [...A, A["length"]]>, keyof any[]>;

type test = GenIndices<3>; //{0: 0; 1: 1; 2: 2; }

最后,需要将索引映射到原始类型键下的元组中的对应值(注意,除非保证P extends keyof T[<key here>],否则将无法使用索引来索引属性):

type Transpose<T extends { value: readonly number[], label: readonly string[] }> = {
    [ P in keyof GenIndices< T["value"]["length"] > ] : P extends keyof T["value"] ? 
    P extends keyof T["label"] ? 
    { value: T["value"][P], label: T["label"][P] } : 
    never : 
    never;
} & readonly any[];

就是这样,让我们​​测试一下该实用程序的工作原理:

const original = { 
    value: [1, 2, 3], 
    label: ["one", "two", "three"] 
} as const;

//Transpose implemenetation

type test = Transpose<typeof original>;

const ok: test = [ 
    {value: 1, label: "one"}, 
    {value: 2, label: "two"},
    {value: 3, label: "three"}
];

const err: test = [ 
    {value: 2, label: "one"}, //Type '2' is not assignable to type '1'
    {value: 2, label: "two"},
    { label: "three" } //Property 'value' is missing
];

transpose 函数可能如下所示:

declare function tranpose<T extends typeof original>(original: T): Transpose<T>;
const test2 = tranpose(original)[2]; //{ value: 3; label: "three"; }

Playground

我明白了,这个问题是关于输入的,而不是那个函数的实现。

function tranpose<T extends Record<keyof any, readonly any[]>>(original: T): {
    [K in keyof T]: T[K] extends readonly (infer U)[] ? U : never
}[] {
    // implementation is up to you
    return [];
}

TS Playground

或作为类型

type Transpose<T extends Record<keyof any, readonly any[]>> = {
    [K in keyof T]: T[K] extends readonly (infer U)[] ? U : never
}[];

这并不关心输入有什么属性或有多少属性,只要它们是某种数组即可。

它不能做的是映射这些数组中的各个索引或确保所有数组具有相同的长度,这是您在实现该功能时必须处理的事情。

嘿,你想让 Transpose<Transpose<T>> 产生类似 T 的东西,对吧?而且我假设,尽管问题中没有明确说明,但您希望它适用于包含对象或数组的 arbitrary 对象或数组,而不是特别是“具有valuelabel 包含数组的属性。

这在概念上很简单,但在处理对象与数组时,事情就变得棘手了。即使 mapped types should produce array/tuples from array/tuples,也存在编译器没有意识到它正在映射 array/tuple 的陷阱,直到为时已晚并且您的映射类型充满了可怕的数组方法,如 "length" | "push" | "pop" |...。我假设你的实现也会有一些问题,但我担心这里的类型,而不是实现。

这是我的版本,除了 IntelliSense 显示相同类型的联合有点奇怪(比如 type Foo = "a" | "a" | "a",你希望看到 type Foo = "a"),幸运的是没有影响类型的行为方式:

type Transpose<T> =
    T[Extract<keyof T, T extends readonly any[] ? number : unknown>] extends
    infer V ? { [K in keyof V]: { [L in keyof T]:
        K extends keyof T[L] ? T[L][K] : undefined } } : never;

declare function transpose<T>(t: T): Transpose<T>;

解释是我们遍历 Tvalue/element 类型来找出键是什么输出类型应该是。那应该是 T[keyof T] 但我们需要做 T[Extract<keyof T, ...] 以确保数组不会把事情搞砸。然后我们主要只是将 T[K][L] 变成 T[L][K] 并在此过程中进行一些类型检查。


让我们测试一下。如果您希望编译器跟踪哪些值位于哪些键,我们需要一个 const assertion 或类似的东西:

const original = {
    value: [1, 2, 3],
    label: ["one", "two", "three"]
} as const
/* const original: {
    readonly value: readonly [1, 2, 3];
    readonly label: readonly ["one", "two", "three"];
} */

现在我们将其转置:

const transposed = transpose(original);
/* readonly [{
    readonly value: 1;
    readonly label: "one";
}, {
    readonly value: 2;
    readonly label: "two";
}, {
    readonly value: 3;
    readonly label: "three";
}]
*/


transposed.forEach(v => v.label) // transposed is seen as an array
transposed[1].label // known to be "two"

看起来不错。如果你在 transposed 上使用 IntelliSense,你会看到它是三个相同类型的联合,但是 ‍♂️。 (这是 TypeScript 的一个已知设计限制;请参阅 microsoft/TypeScript#16582. It's possible to force the compiler to aggressively reduce unions, as shown here,但这并不是这个问题的重点,所以我离题了。)输出类型被视为一个元组,因此它具有所有数组方法,我假设你想要。

那么我们应该可以通过转置的方式再次获得原件:

const reOriginal = transpose(transposed);
/* const reOriginal: {
    readonly value: readonly [1, 2, 3];
    readonly label: readonly ["one", "two", "three"];
} */

reOriginal.label.map(x => x + "!"); // reOriginal.label is seen as an array

再次,看起来不错。 reOriginal 的类型(模 IntelliSense)与 original 的类型相同。万岁!


Playground link to code

Playground link to code with IntelliSense fix

这是替代版本:



type Origin = Readonly<{
    value: ReadonlyArray<any>,
    label: ReadonlyArray<any>
}>
type Reduce<T extends Origin, Result extends ReadonlyArray<any> = []> =
    T extends { value: [], label: [] }
    ? Result
    : T extends { value: [infer V], label: [infer L] }
    ? [...Result, { value: V, label: L }]
    : T extends { value: [infer Val, ...infer Vals], label: [infer Label, ...infer Labels] }
    ? Reduce<{ value: Vals, label: Labels }, [...Result, { label: Label, value: Val }]>
    : Result


type Test = Reduce<{ label: [1, 2, 3], value: ['1','2', '3'] }>

Playground