Search code examples
javascripttypescripttypescript-typings

How to omit and delete part of the value from an object in TypeScript correctly?


The useHider function was used to hide some values from an object with corret type, like if I use const res = useHider({ id: 1, title: "hi"}, "id"), it will come back with { title: "hi" } only, and when I try to use res.id, it will come with error in TypeScript.

The hideQueryResult function allow me to hide createdAt, updatedAt and deletedAt by default, or I can add some params to hide more values from an object.

const useHider = <T, K extends keyof T>(obj: T, keysToHide: K[]) => {
    let res = obj;

    keysToHide.forEach((key) => {
        delete res[key];
    });

    return res as Omit<T, K>;
};

const hideQueryResult = <T, K extends keyof T>(
    query: T,
    keysToHide: K[] = [],
) => {
    const at = ["createdAt", "updatedAt", "deletedAt"] as K[];
    const allKeysToHide = [...keysToHide, ...at];
    const res = useHider(query, allKeysToHide);
    return res;
};

But when I try to use hideQueryResult to hide some values, it do not work as I expected.

const source = {
    id: "01ABC",
    title: "123",
    createdAt: "ABC",
    updatedAt: "ABC",
    deletedAt: "ABC",
};

const res1 = useHider(source, ["createdAt", "updatedAt", "deletedAt"]);

console.log(res1.id); // success (expected)
console.log(res1.createdAt); // failed (expected)

const res2 = hideQueryResult(source);

console.log(res2.id); // failed (not expected)

const res3 = hideQueryResult(source, ["id"]);

console.log(res3.createdAt); // success (not expected)

What can I do to make it work?


Solution

  • you should first make a shallow copy from the obj. because if you don't, the res will be reference to obj. so, if you try to delete a property from res, it will actually delete from obj. but with this syntax ({...obj}) you can make a shallow copy and delete/add the properties you want.

    const useHider = <T, K extends keyof T>(obj: T, keysToHide: K[]) => {
        let res = {...obj}; // edited line
    
        keysToHide.forEach((key) => {
            delete res[key];
        });
    
        return res as Omit<T, K>;
    };
    

    note: shallow copy will only copy the first keys and values, the values may still refer to the main values. so:

    const obj = { 
        name: "John", 
        data: {
            age: 10
        }
    }
    
    const clone = {...obj}
    
    clone.username = "Alex"
    console.log(obj.username) // John
    
    clone.data.age = 20
    console.log(obj.data.age) // 20
    
    clone.data = null
    console.log(obj.data) // { age: 20 }
    

    Now, to solve the type issues. you need to tell the typescript that the object that goes to the useHider through hideQueryResult can have those 3 fields. and then give it a default value, so when we don't give the 2nd argument, typescript will not infer every single key automatically and instead use the never type as the default value which means there is not any set data.

    const fieldsToRemove = ["createdAt", "updatedAt", "deletedAt"] as const // `const` makes it readonly and type-friendly
    
    const hideQueryResult = <T, K extends keyof T = never>( // `never` means no data is set
        query: T,
        keysToHide: K[] = [],
    ) => {
        const res = useHider(
            query as T & Record<typeof fieldsToRemove[number], unknown>, // Record makes an object type. (first arg is the keys and 2nd arg is the value type)
            [...fieldsToRemove, ...keysToHide]
        );
        return res;
    };