The main issue you're running into here is that Object.entries(items)
's typing does not generally return a strongly-typed array of entries corresponding to the known properties of items
. Instead, returns Array<[string, unknown]>
(that is unknown
, not undefined
as your comment mentions).
This is because TypeScript's object types aren't exact. A value of the type {foo: string}
must have a string
-valued foo
property, but it might have any other properties of any other type. The canonical SO question/answer about this sort of issue is Why doesn't Object.keys return a keyof type in TypeScript?, where Object.keys(obj)
returns string[]
instead of Array<keyof typeof obj>
.
So, technically speaking, the compiler is correct to say it doesn't know what v
is there. I don't know what you're really going to do inside of doSomething()
, but it is technically possible for the recursive call to hit non-Items
properties where Object.entries()
gives an error. For example:
interface Foo {
a: {}
}
interface Bar extends Foo {
b: null;
}
const bar: Bar = {a: {}, b: null};
const foo: Foo = bar;
doSomething(foo); // ERROR AT RUNTIME! Can't convert null to object
The compiler accepts doSomething(foo)
because foo
is of type Foo
, which matches Items<{a: unknown}>
. But it turns out foo
is also of type Bar
(which extends Foo
and is therefore a valid Foo
). And when you call doSomething(foo)
you will hit a runtime error, because Object.entries(foo)
returns at least one entry you didn't expect.
If you are not worried about such edge cases, you could use a type assertion to give a stronger type to the return value of Object.entries()
:
type Entry<T> = { [K in keyof T]-?: [K, T[K]] }[keyof T]
function doSomething<T>(items: Items<T> | {}) {
for (const [k, v] of Object.entries(items) as Array<Entry<Items<T>>>) {
console.log(k, v);
doSomething(v); // okay now
}
}
The type Array<Entry<Items<T>>>
is probably what you imagine Object.entries(items)
to return assuming that items
's type were exact/closed... Array<Entries<Foo>>
is, for example, Array<["a", {}]>
. This will cause the compiler to infer that v
is of type Items<T>[keyof T]
which is going to be some kind of object
.
Again, this doesn't prevent the edge cases like doSomething(foo)
mentioned above; it just assumes they won't happen.
Also, if you didn't notice, I needed to widen the type of items
from Items<T>
to Items<T> | {}
, so that recursive calls can be seen as safe by the compiler. Hopefully that is acceptable to you.
Playground link to code