Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 704
Daniel Santos Avatar asked Feb 21 '21 13:02

Daniel Santos


People also ask

How to create transposed variables in a data set?

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.

How to transpose data when Excel transpose is not working?

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.

What is the difference between data and Proc transpose?

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.

What does _name_ mean in a transposed procedure?

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.


5 Answers

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

like image 178
jcalz Avatar answered Nov 12 '22 09:11

jcalz


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

like image 38
Oleg Valter is with Ukraine Avatar answered Nov 12 '22 08:11

Oleg Valter is with Ukraine


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.

like image 38
Thomas Avatar answered Nov 12 '22 08:11

Thomas


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

like image 37
captain-yossarian Avatar answered Nov 12 '22 08:11

captain-yossarian


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

like image 24
Gabriel Borges Avatar answered Nov 12 '22 07:11

Gabriel Borges