I'm not sure I'd recommend this solution, but since you want , here you have:
type KeysUnion<T, Cache extends string = ''> =
Record<string, unknown> extends T ? string :
T extends PropertyKey ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${P}`>
: never
}[keyof T]
const isObject = (obj: unknown): obj is Record<string, unknown> =>
typeof obj === 'object' && obj !== null && !Array.isArray(obj)
type Primitives = string | number | boolean | symbol | null | undefined | bigint
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type ExtractFromObj<
Obj,
Keys,
Result = {}
> =
(Keys extends string[]
? (Obj extends Primitives
? Result
: {
[Prop in keyof Obj]:
(Prop extends Keys[number]
? Result & Pick<Obj, Prop>
: (Obj[Prop] extends Primitives
? Result
:ExtractFromObj<Obj[Prop], Keys, Result>)
)
}[keyof Obj])
: never)
function extractFromObj<
Obj extends Record<string, unknown>,
Keys extends KeysUnion<Obj>,
InferedKeys extends Keys[]
>(
obj: Obj,
keys: [...InferedKeys],
result?: UnionToIntersection<ExtractFromObj<Obj, [...InferedKeys]>>
): UnionToIntersection<ExtractFromObj<Obj, [...InferedKeys]>>
function extractFromObj(
obj: Record<string, unknown>,
keys: string[],
result: Partial<Record<string, unknown>> = {}
) {
if (Object.keys(obj).length === 0) {
return result
}
return Object.keys(obj).reduce((acc, prop) => {
if (keys.includes(prop)) {
return {
...acc,
[prop]: obj[prop]
}
}
const reduced = obj[prop];
if (isObject(reduced)) {
return extractFromObj(reduced, keys, { ...acc, ...result })
}
return acc
}, result)
}
const foo = extractFromObj({
'id':'hello',
"foo": {
"bar": {
"baz": {
"properties": "hello world"
}
}
}
}, ['id','baz']) // ok
foo.id // string
foo.baz // { "properties": "hello world" }
const foo2 = extractFromObj({
"object": "page",
"id": "b03f9945-528a-4a1c-a280-6972bbc49462",
"created_time": "2021-11-07T11:24:00.000Z",
"last_edited_time": "2021-11-07T11:24:00.000Z",
"cover": null,
"foo": {
"bar": {
"baz": {
"properties": "hello world"
}
}
}
}, ['id', 'bazzz']) // expected error
Playground
extractFromObj - iterates recursively through each object property and check wheter it exists in keys or not. If exists - merge it with result.
KeysUnion - is described thoroughly in this answer. See also related links
Also, IntelliSense doesn't seem to work when trying to restructure
I just thought you will not ask
Here is the code which is responsible for iteratinng through Obj keys recursively and obtaining appropriate value.
type ExtractFromObj<
Obj,
Keys,
Result = {}
> =
(Keys extends string[]
? (Obj extends Primitives
? Result
: {
[Prop in keyof Obj]:
(Prop extends Keys[number]
? Result & Pick<Obj, Prop>
: (Obj[Prop] extends Primitives
? Result
:ExtractFromObj<Obj[Prop], Keys, Result>)
)
}[keyof Obj])
: never)
Reducer - does basically the same as extractFromObj function - but in type scope.

Generic arguments Obj, Keys, Result represents function extractFromObj arguments and have same semantic meaning.
If Keys is empty tuple (Keys extends string[]), just like Object.keys(obj).length === 0 - we need ti return Result.
[Prop in keyof Obj]: - iterates through Obj keys, just like Object.keys(obj).reduce.
Prop extends Keys[number] - checks whether iterated property Prop exists in the union of all Keys. Just like keys.includes(prop).
If Prop existsm in the Keys return merged accumulator with approapriate key/value pair: Result & Pick<Obj, Prop> just like
return {
...acc,
[prop]: obj[prop]
}
If Prop does not exists in Keys and Obj[Prop] is an object, call recursively
(Obj[Prop] extends Primitives ? Result:ExtractFromObj<Obj[Prop], Keys, Result>)
just like
if (isObject(reduced)) {
return extractFromObj(reduced, keys, { ...acc, ...result })
}
[keyof Obj] - at the end, gathers all object values in a union and UnionToIntersection merges it.
That's it.