Given an object and a string array of keys (these keys will exist in the object), construct a new object containing those keys and its corresponding value with the correct Typescript typing. That is:
/**
* This function will construct a new object
* which is the subset values associated to
* the keys array
**/
function extractFromObj<T>(obj: T, keys: (keyof T)[])
// in here, suppose `post` is a huge object,
// but the intellicence should only show keys
// from the keys array
const { id, properties, created_time } = extractFromObj(post, ['id', 'properties', 'created_time'])
function extractFromObj<T>(obj: T, keys: (keyof T)[]): Record<keyof typeof keys, any> {
return keys.reduce((newObj, curr) => {
newObj[curr] = obj[curr]
return newObj
}, {} as Record<keyof typeof keys, any>)
}
const {} = extractFromObj(post, ['id', 'properties', 'created_time']) // wrong
Assistance needed please & thank you 🙏
I forgot to mention, the solution should also work for when a key is nested inside the object. For example:
const obj = {
"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"
}
}
}
}
It looks like you are looking for the Pick<Type, Keys> utility type.
You may implement your function as follows:
const post = {
id: 1,
properties: 2,
created_time: 'aaa',
other_prop: 2
}
function extractFromObj<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
return keys.reduce((newObj, curr) => {
newObj[curr] = obj[curr]
return newObj
}, {} as Pick<T, K>)
}
const result1 = extractFromObj(post, ['id', 'properties', 'created_time'])
On top of that:
Object.fromEntriesconst post = {
id: 1,
properties: 2,
created_time: 'aaa',
other_prop: 2
}
function extractFromObjEs2019<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
const entries = keys.map(key => ([key, obj[key]]));
return Object.fromEntries(entries);
}
const result2 = extractFromObjEs2019(post, 'id', 'properties', 'created_time')
Playground link
Update:
For recursive version, you may start with this playground, based on Typescript recursive subset of type
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With