Building on @Pvl's answer, you can include type safety on your returned value as well if you use overrides:
function dig<
T,
K1 extends keyof T
>(obj: T, key1: K1): T[K1];
function dig<
T,
K1 extends keyof T,
K2 extends keyof T[K1]
>(obj: T, key1: K1, key2: K2): T[K1][K2];
function dig<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2]
>(obj: T, key1: K1, key2: K2, key3: K3): T[K1][K2][K3];
function dig<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3]
>(obj: T, key1: K1, key2: K2, key3: K3, key4: K4): T[K1][K2][K3][K4];
function dig<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4]
>(obj: T, key1: K1, key2: K2, key3: K3, key4: K4, key5: K5): T[K1][K2][K3][K4][K5];
function dig<
T,
K1 extends keyof T,
K2 extends keyof T[K1],
K3 extends keyof T[K1][K2],
K4 extends keyof T[K1][K2][K3],
K5 extends keyof T[K1][K2][K3][K4]
>(obj: T, key1: K1, key2?: K2, key3?: K3, key4?: K4, key5?: K5):
T[K1] |
T[K1][K2] |
T[K1][K2][K3] |
T[K1][K2][K3][K4] |
T[K1][K2][K3][K4][K5] {
let value: any = obj && obj[key1];
if (key2) {
value = value && value[key2];
}
if (key3) {
value = value && value[key3];
}
if (key4) {
value = value && value[key4];
}
if (key5) {
value = value && value[key5];
}
return value;
}
Example on playground.