Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create a new subset object from array of keys with the correct type infer

Tags:

typescript

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'])

My solution (wrong so far)

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 🙏

Update

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"
      }
    }
  }
}

like image 588
Humbledore Avatar asked Oct 24 '25 04:10

Humbledore


2 Answers

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:

  • you may prefer to use vargargs over an array for keys
  • if you target ES2019, you can use Object.fromEntries
const 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

like image 189
Lesiak Avatar answered Oct 26 '25 17:10

Lesiak


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. enter image description here

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.

like image 28
captain-yossarian Avatar answered Oct 26 '25 19:10

captain-yossarian



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!