I'm unsure why you want this, but here goes...
Using ideas from this answer to convert a mapped type to an intersection of functions that we can then match against different type parameters, we can define ToTuple
in a way that will work for a fixed maximum number of members of the original interface.
type IntersectionOfValues<T> =
{[K in keyof T]: (p: T[K]) => void} extends
{[n: string]: (p: infer I) => void} ? I : never;
type IntersectionOfFunctionsToType<F, T> =
F extends {
(na: infer NA, a: infer A): void;
(nb: infer NB, b: infer B): void;
(nc: infer NC, c: infer C): void;
} ? [NA, A, NB, B, NC, C] :
F extends {
(na: infer NA, a: infer A): void;
(nb: infer NB, b: infer B): void;
} ? [NA, A, NB, B] :
F extends {
(na: infer NA, a: infer A): void
} ? [NA, A] :
never;
type ToTuple<T> = IntersectionOfFunctionsToType<
IntersectionOfValues<{ [K in keyof T]: (k: K, v: T[K]) => void }>, T>;
interface Person {
name: string,
age: number
}
type Args = ToTuple<Person> // ['name', string, 'age', number]
An alternative approach would be to use UnionToIntersection
on keyof
the interface, but I believe that going through a union may carry a greater risk of losing the order of the members of the interface. (I believe I've seen unions lose order in the past, though I couldn't reproduce it in tests in the playground just now.) In the solution above, it's well-established that intersections are ordered, so we are only relying on the mapped types to preserve order and the inference process in IntersectionOfValues
to generate the contravariant candidates in order and intersect them in order. This is still implementation-dependent behavior, but I think it's unlikely to change.