Решение за один проход с поддержкой массивов и таплов. Глубина любая.
https://www.typescriptlang.org/play/?#code/C4TwDgp...
Основное отличие от решения
Alexandroppolus, ключи собираются последовательно и не парсятся, но результат, понятное дело, одинаковый.
type JoinKeys<T extends object> = JoinKeysDeep<T> extends infer J extends Record<
string,
unknown
>
? { [Key in J extends J ? keyof J : never]: J extends J ? J[Key] : never }
: never;
type JoinKeysDeep<
T extends object,
K extends keyof T = JoinKeysDeep.GetKeys<T> & keyof T,
> = K extends K & (string | number)
? T[K] extends object
? JoinKeysDeep<T[K]> extends infer J extends Record<string, unknown>
? J extends J
? Record<`${K | '*'}.${keyof J & (string | number)}`, J[keyof J]>
: never
: never
: Record<K | '*', T[K]>
: never;
namespace JoinKeysDeep {
export type GetKeys<T> = T extends unknown[]
? keyof T &
(number extends T['length']
? // array
number
: // tuple
`${number}`)
: keyof T;
}