skip to Main Content

I have been trying to make a declaration for the RecursiveOmit and RecursivePick for cloning methods JSON.parse(JSON.stringify(obj, [‘myProperty’]))

type RecursiveKey<T> = T extends object ? keyof T | RecursiveKey<T[keyof T]> : never; 

type RecursivePick<T, K> = {
    [P in keyof T]: P extends K ? (T[P] extends object ?  RecursivePick<T[P], K> : T[P]) : never
}

type RecursiveOmit<T, K> = {
    [P in keyof T]: P extends K ? never : (T[P] extends object ? RecursiveOmit<T[P], K> : T[P])
}

const clone = <T, K extends RecursiveKey<T>>(object: T, whiteListedProperties: K[]): RecursivePick<T, K> => { 
    return JSON.parse(JSON.stringify(object, whiteListedProperties as (string | number)[])); 
}

const cloneWithBlackList = <T, K extends RecursiveKey<T>>(object: T, blackListedProperties: K[]): RecursiveOmit<T, K> => {
  return JSON.parse(JSON.stringify(object, (key: string, value: any): any => blackListedProperties.includes(key as K) ? undefined : value));
};


const c = {
    a: {
        a: 1,
        b: 2,
        c: 3
    },
    b: {
        a: 1,
        b: 2
    }
}

const cc = clone(c, ['b']);


cc.b.a = 2 // error a shouldn't exists on cc.b 
cc.b.b = 2; // b should exists on cc.b
cc.a.c = 2 // error c shouldn't exists on cc.a 

const cb = cloneWithBlackList(c, ['b']);

cb.a.a = 2; // a should exists on cb.a  
cb.b.b = 3; // error b shouldn't exists on cb 
cb.a.c = 2; // c should exists on cb.a 

Playground

I have been trying all kinds of variations of this and different/similar answers from other questions. But I was not able to get it working.

Anyone has any idea what I am doing wrong?

2

Answers


  1. This seems like a bug in TypeScript to me. I have not found any issues on GitHub pertaining to this issue, but nevertheless, the workaround I have found is as follows. First, clean up the type definitions with key-remapping, and Extract/Exclude:

    type RecursivePick<T, K extends PropertyKey> = {
        [P in Extract<keyof T, K>]: T[P] extends object
            ? RecursivePick<T[P], K>
            : T[P];
    };
    
    type RecursiveOmit<T, K extends PropertyKey> = {
        [P in Exclude<keyof T, K>]: T[P] extends object
            ? RecursiveOmit<T[P], K>
            : T[P];
    };
    

    Then we can change the functions, using 5.0 const generics:

    const clone = <T, const K extends RecursiveKey<T>>(
        object: T,
        whiteListedProperties: K[]
    ): RecursivePick<T, K> => {
        return JSON.parse(JSON.stringify(object, whiteListedProperties as any));
    };
    
    const cloneWithBlackList = <T, const K extends RecursiveKey<T>>(
        object: T,
        blackListedProperties: K[]
    ): RecursiveOmit<T, K> => {
        return JSON.parse(
            JSON.stringify(object, (key: any, value: any): any =>
                blackListedProperties.includes(key) ? undefined : value
            )
        );
    };
    

    Playground

    Login or Signup to reply.
  2. As you can see from hovering over the function call, the "problem" is that K is inferred to be "a" | "b" | "c" (i.e. RecursiveKey<typeof c>) and not just "b".

    You can fix that by being explicit (clone<typeof c, "b">(c, ["b"]) or clone(c, ['b' as const])), but also TypeScript 5.0 has just the solution for this problem: const type parameters:

    const clone = <T, const K>(object: T, whiteListedProperties: K[]): RecursivePick<T, K> => {…}
    
    const cloneWithBlackList = <T, const K>(object: T, blackListedProperties: K[]): RecursiveOmit<T, K> => {…}
    

    (updated playground)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search