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