To avoid the issues of trying to get the TypeScript compiler to know or care about property order in object types (which are many; keyof
will produce a union, whose order is implementation-defined, and so you can get something out of that but it's likely to be different from what you expect; see How to transform union type to tuple type for more information) we will explore your "another possible solution" which uses a tuple-of-tuples to start with instead of an object type.
We need a function like argsTupleListToTupleType()
which takes an input whose type is an array of key-value pairs (each of which have a keylike type for its first element) and converts it into an array of just values. We can express this in the type system as
declare function argsTupleListToTupleType<T extends any[]>(
argsTupleList: readonly [...{ [I in keyof T]: readonly [PropertyKey, T[I]] }]
): T;
This function is generic in the output type T
, and the argsTupleList
input argument is of a mapped tuple type on T
, where each element of T
has been replaced with a readonly
pair with a keylike first member and the element from T
as the second member. (Note that readonly
tuples accept more inputs than non-readonly
tuples, so this is more accommodating.) Since this is a homomorphic mapped type (see What does "homomorphic mapped type" mean? ) the compiler is able to infer T
from the input value of the mapped type.
Also note that I made the type of argsTupleList
a readonly
variadic tuple type, so readonly [...XXX]
instead of just XXX
, which gives the compiler a hint that it should interpret argsTupleList
as a tuple type where possible, and that it should accept any const
-asserted inputs. Essentially this wrapping just makes the function more accommodating of being called in different ways, without really changing the basic operation.
So that's the type system, what about implementation? Well that's a straightforward argsTupleList.map(x => x[1])
using the map()
array method. The TypeScript compiler can't possibly verify that this satisfies the call signature, so we'll need to use a type assertion to tell it that we've done the checking ourselves and that it shouldn't worry. (See Mapping tuple-typed value to different tuple-typed value without casts for more information):
function argsTupleListToTupleType<T extends any[]>(
argsTupleList: readonly [...{ [I in keyof T]: readonly [PropertyKey, T[I]] }]
): T {
return argsTupleList.map(x => x[1]) as T;
}
Let's try it out with your example:
type TupleListType = [
[string, number],
[string, string],
[string, boolean],
[string, string],
];
const argsTupleList: TupleListType = [
["prop1", 1],
["prop2", "two"],
["prop3", true],
["prop4", "four"],
]
const tuple = argsTupleListToTupleType(argsTupleList);
// const tuple: [number, string, boolean, string]
console.log(tuple); // [1, "two", true, "four"]
Looks good. Both the type and value of tuple
are what we expect. This also works with different inputs:
const t2 = argsTupleListToTupleType([
["a", 1], ["b", true], ["c", new Date()]
]);
// const t2: [number, boolean, Date]
console.log(t2); // [1, true, Date: "2023-02-28T16:47:56.073Z"]
const inp = [["x", 0], ["y", null], ["z", undefined]] as const;
const t3 = argsTupleListToTupleType(inp); // const t3: [0, null, undefined]
console.log(t3); // [0, null, undefined]
Playground link to code