如何声明类型的“转置”版本?
我想定义一个 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,对吧?而且我假设,虽然在问题中没有明确说明,但您希望它适用于包含对象或数组的任意对象或数组,而不是专门用于“包含数组的事物value和label属性”。
这在概念上很容易,但在处理对象与数组时事情会变得棘手。尽管映射类型应该从 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(或者在value和label键下的可变数组元素的类型将被加宽)。
有一种方法可以在不使用生成索引的情况下键入转置,从而生成更简洁和健壮的类型(其余逻辑与下面的实用程序相同):
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"; }
操场