Note that the desired dictToArray
function type can run afoul of the problem mentioned in Why doesn't Object.keys return a keyof type in TypeScript?, whereby an object types in TypeScript are open and values can contain more properties at runtime than are known to the compiler. This:
function keys<T extends object>(obj: T) {
return Object.keys(obj) as Array<keyof T>;
}
will work just fine when you pass it an object literal, but have possibly unexpected results if you pass it an instance of a class or interface which has been extended with extra properties. As long as you are aware of this problem and willing to face the consequences if they arise, great.
Anyway, my inclination here for a dictToArray()
typing would look like this:
type DictArray<T extends object> =
Array<{ [K in keyof T]: [K, Pick<T, K>] }[keyof T]>;
declare function dictToArray<T extends object>(obj: T): DictArray<T>;
We are making a mapped type which walks through each key K
in keyof T
and generates the tuple [K, Pick<T, K>]
for it. If T
looks like {x: 1, y: 2}
, this makes something like {x: ["x", {x: 1}], y: ["y", {y: 2}]}
, which has the property values you want but is still nested inside an object type... so we index into this object with keyof T
to get the desired union ["x", {x: 1}] | ["y", {y: 2}]
.
You can verify that this gives you a reasonable type:
const arr = dictToArray(dict);
/* const arr: DictArray<{
foo: number;
bar: number;
baz: string;
}> */
Oh, well, that doesn't actually show much. Let's force the compiler to expand that out:
type ExpandRecursively<T> =
T extends object ? { [K in keyof T]: ExpandRecursively<T[K]> } : T;
type ArrType = ExpandRecursively<typeof arr>;
/* type ArrType = (
["foo", { foo: number; }] |
["bar", { bar: number; }] |
["baz", { baz: string; }]
)[] */
So, you can see that arr
has a type which is an array of strongly typed tuples. Hooray!
Oh, wait, I see that you want this type:
[
["foo", { foo: 1 }],
["bar", { bar: 2 }],
["baz", { baz: "hi" }]
]
which, aside from the fact that the compiler has no knowledge of the literal values of the object properties (you let the compiler infer the types so they are widened to number
and string
as opposed to 1
, 2
, or "hi"
)... ugh, I digress.
I see that you want this type:
[
["foo", { foo: number }],
["bar", { bar: number }],
["baz", { baz: string }]
]
which seems to represent the order of the entries that come out. Object property order is technically observable in JavaScript, although it doesn't always behave the way people expect (Does JavaScript guarantee object property order?). But in TypeScript types, object property order is not supposed to be observable. The type {foo: number, bar: number, baz: string}
is equivalent to the type {bar: number, baz: string, foo: number}
. There's no type system test you should be doing which could distinguish one from the other. It is sometimes possible to tease order information out of the type system, but it is a terrible idea because it will be nondeterministic (How to transform union type to tuple type).
So there's no principled way to ask the compiler to give you the above type as opposed to, say,
[
["foo", { foo: number }],
["baz", { baz: string }],
["bar", { bar: number }]
]
or any other ordering. It is within the realm of conceivability that we could make DictArray<T>
give you the union of all possible orderings, but this would scale very badly and probably not be particularly usable even for small cases. So unless you are morbidly curious about how to generate that, I won't bother.
Playground link to code