Below naive version, naive it means I don't restrict path to be valid path of the object, if keys path is invalid the return type will be unknown.
type ExtractType<O, T extends Array<any>> = {
[K in keyof O]:
((...a: T) => any) extends ((a: any, ...args: infer Tail) => any)
? Tail['length'] extends 0 ? O[K] : ExtractType<O[K], Tail> : never
}[T[0]]
// example Foo is number
type Foo = ExtractType<{ a: { b: { c: {d: number} } }, g: string }, ['a', 'b', 'c', 'd']>
It is recursive type so depth is not restricted. Few things to consider:
((...a: T) => any) extends ((a: any, ...args: infer Tail) => any) this part is used in order to cut the Head of the tuple, and work recursively with Tail. It means we traverse the object until tuple with keys will be empty. Every recursive call work with one element less.
Tail['length'] extends 0 ? O[K] : ExtractType<O[K], Tail> - we take the value type only when we are in the end of the key tuple