I'm close to nailing it, but can't find a way around the final TS2322: Type  TcolTuple[i]  is not assignable to type  string | number | symbol compiler error.

So, here is a utility function rowsToObjects() that quite a few people probably defined in their projects once or twice, it's somewhat similar to zip() in concept:

const objects = rowsToObjects(
    ['id', 'color' , 'shape'   , 'size'  , 'to' ] as const,  
    [  1n, 'red'   , 'circle'  , 'big'   , '0x0'] as const,
    [  2n, 'green' , 'square'  , 'small' , '0x0'] as const,
    [  3n, 'blue'  , 'triangle', 'small' , '0x0'] as const,
)

That outputs:

[
    {id: 1n, color: 'red', shape: 'circle', size: 'big', to: '0x0'},
    {id: 2n, color: 'green', shape: 'square', size: 'small', to: '0x0'},
    {id: 3n, color: 'blue', shape: 'triangle', size: 'small', to: '0x0'},
]

The actual implementation is obviously trivial, but typing it gives me some hard time:

export function rowsToObjects<
    Tobj extends { [i in keyof TcolTuple as TcolTuple[i]]: TvalTuple[i] },
    TcolTuple extends readonly string[],
    TvalTuple extends { [j in keyof TcolTuple]: unknown }
>(cols: TcolTuple, ...rows: TvalTuple[]): Tobj[];

Current code seems logical to me, but the compiler complains about the as TcolTuple[i] part:

TS2322: Type  TcolTuple[i]  is not assignable to type  string | number | symbol 
  Type  TcolTuple[keyof TcolTuple]  is not assignable to type  string | number | symbol 
    Type
    TcolTuple[string] | TcolTuple[number] | TcolTuple[symbol]
    is not assignable to type  string | number | symbol 
      Type  TcolTuple[string]  is not assignable to type  string | number | symbol 

Am I missing something obvious here? The typing is close to satisfactory, but without that as TcolTuple[i] it does not recognize which value belongs to which key and just unions them all.

enter image description here

1

There are 1 answers

0
jcalz On BEST ANSWER

I think the main problem you're having with

{ [I in keyof TcolTuple as TcolTuple[I]]: TvalTuple[I] }

is that using key remapping prevents the mapped type from being homomorphic (see What does "homomorphic mapped type" mean?), so instead of mapping over just the numeric-like indices of the tuple like "0" | "1" | "2", you're mapping over all the indices, including number, a mixture of all the elements. And that gives you the union you're unhappy with.

The easiest change here is to explicitly map over only the numeric-like indices, by intersecting keyof TcolTuple with the pattern template literal type `${number}` (as implemented in microsoft/TypeScript#40598. That removes anything that isn't a string version of a number. For example, "0" | "1" | "2" | number | "length" | "find" when intersected with `${number}`, gives you just "0" | "1" | "2".

That more or less fixes it:

declare function rowsToObjects<
  Tobj extends { [I in `${number}` & keyof TcolTuple as TcolTuple[I]]: TvalTuple[I] },
  TcolTuple extends readonly string[],
  TvalTuple extends { [J in keyof TcolTuple]: unknown }
>(cols: TcolTuple, ...rows: TvalTuple[]): Tobj[];

const objects = rowsToObjects(
  ['id', 'color', 'shape', 'size', 'to'] as const,
  [1n, 'red', 'circle', 'big', '0x0'] as const,
  [2n, 'green', 'square', 'small', '0x0'] as const,
  [3n, 'blue', 'triangle', 'small', '0x0'] as const,
)
/* const objects: {
    id: 1n | 2n | 3n;
    color: "red" | "green" | "blue";
    shape: "circle" | "square" | "triangle";
    size: "big" | "small";
    to: "0x0";
}[] */

Personally, if I were writing this for myself, I would:

  • use const type parameters instead of requiring callers use const assertions;
  • maintain the tuple-type of the inputs so that if the input array is strongly ordered then so is the output (e.g., rowsToObjects(["a"],[0],[1]) should return [{a: 0}, {a: 1}] and not {a: 0 | 1}[];
  • remove the extra generic type parameters and just compute the output inline instead of relying on default type arguments;
  • use uppercase letters for mapped type parameters like I and J instead of i and j, keeping to the naming convention to distinguish types from variables (in {[P in K]: F<P>} P is a type parameter, not a variable name, so p could be confusing).

None of these are of vital importance, but it gives the output

declare function rowsToObjects<
  const K extends readonly PropertyKey[],
  const V extends readonly Record<keyof K, unknown>[]
>(
  cols: K, ...rows: V
): { [I in keyof V]:
    { [J in `${number}` & keyof K as K[J]]:
      V[I][J]
    }
  };

const objects = rowsToObjects(
  ['id', 'color', 'shape', 'size', 'to'],
  [1n, 'red', 'circle', 'big', '0x0'],
  [2n, 'green', 'square', 'small', '0x0'],
  [3n, 'blue', 'triangle', 'small', '0x0'],
)
/* const objects: readonly [{
    id: 1n;
    color: "red";
    shape: "circle";
    size: "big";
    to: "0x0";
}, {
    id: 2n;
    color: "green";
    shape: "square";
    size: "small";
    to: "0x0";
}, {
    id: 3n;
    color: "blue";
    shape: "triangle";
    size: "small";
    to: "0x0";
}] */

Playground link to code