I would like to define a TS function to transpose an object to array for instance:
const original = {
value: [1, 2, 3],
label: ["one", "two", "three"]
}
const transposed = [
{value: 1, label: "one"},
{value: 2, label: "two"},
{value: 3, label: "three"}
]
Then I would like to declare a function that receives the original
and outputs transposed
and vice versa, like:
function tranpose(original: T): Array<T> // Incorrect, the output should be a transposed version of the object.
How can a define the transposed version of the object and correctly declare the function?
--
PS I'm not asking about implementations, just about the typing and declaration.
To create transposed variable, the procedure transposes the values of an observation in the input data set into values of a variable in the output data set. What Types of Transpositions Can PROC TRANSPOSE Perform? The following example illustrates a simple transposition. In the input data set, each variable represents the scores from one tester.
Transpose Data doesn’t have any link with the original data set. So, if you don’t need the original set of data then just delete it. However, if any changes are done in the original data set then it won’t get reflected in your transposed data, as it is just a copy. Let’s transpose Excel data even when your Excel transpose function is not working.
The TRANSPOSE procedure can often eliminate the need to write a lengthy DATA step to achieve the same result. Further, the output data set can be used in subsequent DATA or PROC steps for analysis, reporting, or further data manipulation. PROC TRANSPOSE does not produce printed output.
Each value of _NAME_ is the name of a variable in the input data set that the procedure transposed. Thus, the value of _NAME_ identifies the source of each observation in the output data set. For example, the values in the first observation in the output data set come from the values of the variable Tester1 in the input data set.
Heh, you want Transpose<Transpose<T>>
to yield something like T
, right? And I'm assuming, although it's not explicit in the question, that you want this to work for arbitrary objects-or-arrays which contain objects-or-arrays, not specifically "things with value
and label
properties that contain arrays".
That is conceptually easy, but things get hairy when dealing with objects-vs-arrays. Even though mapped types should produce array/tuples from array/tuples, there are pitfalls where the compiler doesn't realize it's mapping an array/tuple until it's too late and your mapped type is full of awful array methods like "length" | "push" | "pop" |...
. I assume your implementation will also have some hairiness with this, but I'm worried about types here, not the implementation.
Here's my version, which works except for bit of weirdness with IntelliSense showing a union of identical types (like type Foo = "a" | "a" | "a"
where you'd expect to see type Foo = "a"
), which luckily doesn't affect how the types behave:
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>;
The explanation is that we are walking though the value/element types of T
to figure out what the keys of the output type should be. That should be T[keyof T]
but we need to do T[Extract<keyof T, ...]
to make sure that arrays don't mess things up. Then we mostly just turn T[K][L]
into T[L][K]
with some type checks along the way.
Let's test it. We need a const
assertion or something like it if you want the compiler to keep track of which values are at which keys:
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"];
} */
Now we'll transpose it:
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"
Looks good. If you use IntelliSense on transposed
you will see it as a union of three identical types, but 🤷‍♂️. (This is a known design limitation of TypeScript; see microsoft/TypeScript#16582. It's possible to force the compiler to aggressively reduce unions, as shown here, but that isn't really the point of this question, so I digress.) The output type is seen as a tuple, so it has all the array methods, which I assume you want.
Then we're supposed to be able to obtain the original again by transposing the transposed thing:
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
Again, looks good. The type of reOriginal
is (modulo IntelliSense) the same as the type of original
. Hooray!
Playground link to code
Playground link to code with IntelliSense fix
If you need a type that checks if the original type is exactly transposed, you first need to use a const assertion as const
on the original (or types of elements of mutable arrays under value
and label
keys will be widened).
There is a way to type the transpose without resorting to generating indices which results in a more concise and robust type (the rest of the logic is the same as in the utility below):
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
Recursive version
Next, you have to generate enough indices to ensure the elements in the resulting tuple are positioned correctly. This can be done with recursive conditional types, but beware of the recursion depth limits (~23 in this implementation):
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; }
Finally, you need to map the indices to the corresponding values in tuples under the keys of the original type (note that unless you guarantee that P extends keyof T[<key here>]
, you will not be able to use indices to index the properties):
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[];
That's it, let's test how the utility works:
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
];
And the transpose
function could look like this:
declare function tranpose<T extends typeof original>(original: T): Transpose<T>;
const test2 = tranpose(original)[2]; //{ value: 3; label: "three"; }
Playground
I get, this question is about the typing, not the implementation of that function.
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
or as a type
type Transpose<T extends Record<keyof any, readonly any[]>> = {
[K in keyof T]: T[K] extends readonly (infer U)[] ? U : never
}[];
This doesn't care what or how many properties the input has, as long as they are some kind of array.
What it can't do is map the respective indices in these arrays or ensure that all arrays have the same length, that's something you have to deal with when implementing the function.
Here is alternative veriosn:
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
You could use generics in the types of the object fields
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With