As for the first example, with dot notation, you can use this example:
type Foo = {
user: {
description: {
name: string;
surname: string;
}
}
}
declare var foo: Foo;
/**
* Common utils
*/
type Primitives = string | number | symbol;
/**
* Obtain a union of all values of object
*/
type Values<T> = T[keyof T]
type Elem = string;
type Acc = Record<string, any>
/**
* Custom user defined typeguard
*/
const hasProperty = <Obj, Prop extends Primitives>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, any> =>
Object.prototype.hasOwnProperty.call(obj, prop);
/**
* Obtain value by object property name
*/
type Predicate<Accumulator extends Acc, El extends Elem> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
/**
* If first argument is empty string, avoid using dot (.)
* If first argument is non empty string concat two arguments andput dot (.)
* between them
*/
type Concat<Fst, Scd> =
Fst extends string
? Scd extends string
? Fst extends ''
? `${Scd}`
: `${Fst}.${Scd}`
: never
: never
{
type Test = Concat<'hello','bye'> // "hello.bye"
}
/**
* Obtain union of all possible paths
*/
type KeysUnion<T, Cache extends string = ''> =
T extends Primitives ? Cache : {
[P in keyof T]:
| Concat<Cache, P>
| KeysUnion<T[P], Concat<Cache, P>>
}[keyof T]
{
// "user" | "user.description" | "user.description.name" | "user.description.surname"
type Test = KeysUnion<Foo>
}
/**
* Get object value by path
*/
type GetValueByPath<T extends string, Cache extends Acc = {}> =
T extends `${infer Head}.${infer Tail}`
? GetValueByPath<Tail, Predicate<Cache, Head>>
: Predicate<Cache, T>
{
// {
// name: string;
// surname: string;
// }
type Test = GetValueByPath<"user.description", Foo>
}
/**
* Obtain union of all possible values,
* including nested values
*/
type ValuesUnion<T, Cache = T> =
T extends Primitives ? T : Values<{
[P in keyof T]:
| Cache | T[P]
| ValuesUnion<T[P], Cache | T[P]>
}>
/**
* Function overloading
*/
function deepPickFinal<Obj, Keys extends KeysUnion<Obj>>
(obj: ValuesUnion<Obj>, keys: Keys): GetValueByPath<Keys, Obj>
function deepPickFinal<Obj, Keys extends KeysUnion<Obj> & Array<string>>
(obj: ValuesUnion<Obj>, ...keys: Keys) {
return keys
.reduce(
(acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc,
obj
)
}
/**
* Ok
*/
const result = deepPickFinal(foo, 'user.description') // ok { name: string; surname: string; }
const result2 = deepPickFinal(foo, 'user') // ok
Playground
As you might have noticed, return type is also infered.
Full description and explanation, you will find in my article.
If you want to use rest arguments, comma separated, please refer to my article