如何声明类型的“转置”版本?

我想定义一个 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 我不是在问实现,只是在问打字和声明。

回答

呵呵,你想Transpose<Transpose<T>>产生类似的东西T,对吧?而且我假设,虽然在问题中没有明确说明,但您希望它适用于包含对象或数组的任意对象或数组,而不是专门用于“包含数组的事物valuelabel属性”。

这在概念上很容易,但在处理对象与数组时事情会变得棘手。尽管映射类型应该从 array/tuples 生成数组/元组,但存在一些陷阱,编译器没有意识到它正在映射一个数组/元组,直到为时已晚,并且您的映射类型充满了像"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>;

解释是我们正在遍历 的/元素类型T以找出输出类型的键应该是什么。那应该是,T[keyof T]但我们需要T[Extract<keyof T, ...]确保数组不会把事情搞砸。然后我们大多只是T[K][L]在此过程中T[L][K]进行一些类型检查。


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

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"

看起来挺好的。如果您使用 IntelliSense,transposed您会看到它是三个相同类型的联合,但是 ???。(这是打字稿的一个已知的设计限制,请参见微软/打字稿#16582这是可能的强制编译器大幅削减工会,如图所示。在这里,但是这是不是真的这个问题的地步,所以我离题了。)的输出类型被视为一个元组,因此它具有我假设您想要的所有数组方法。

然后我们应该能够通过转置转置的东西再次获得原始:

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 链接到代码

带有 IntelliSense 修复的代码的 Playground 链接


回答

如果您需要一个检查原始类型是否完全转置的类型,您首先需要在原始类型上使用const 断言 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" }
];

操场


递归版本

接下来,您必须生成足够的索引以确保结果元组中的元素被正确定位。这可以通过递归条件类型来完成,但要注意递归深度限制(在这个实现中为 ~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"; }

操场


以上是如何声明类型的“转置”版本?的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>