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.
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:
const
type parameters instead of requiring callers use const
assertions;rowsToObjects(["a"],[0],[1])
should return [{a: 0}, {a: 1}]
and not {a: 0 | 1}[]
;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";
}] */