1

I have a question about the behavior of the delete keyword in JavaScript or TypeScript. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete

What I need is a way of picking properties from an Object and a way of omitting properties from an Object.

TypeScript comes with a build in type Pick https://www.typescriptlang.org/docs/handbook/advanced-types.html

type Pick<T, K extends keyof T> = { [P in K]: T[P]; }

The opposite of Pick is Omit, which can be implemented like this:

export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

I also wrote some methods for it that take an object and an Array of properties from that object those properties will be picked or omitted from the object.

export let pickMany = <T, K extends keyof T>(entity: T, props: K[]) => {
   return props.reduce((s, prop) => (s[prop] = entity[prop], s) , {} as 
 Pick<T, K>)
}

export let omitMany = <T, K extends keyof T>(entity: T, props: K[]): 
      Omit<T, K> => {
  return props.reduce((s, prop) => (delete s[prop] ,s), entity)
 }

For Omit I used the delete keyword, at the beginning it seem to work but now I am running into some issues. The main issue is that for omitMany the original object is modified. Which causes problems with preserving the original data and preserving state later in my program.

I have written a simple example to illustrate my problem:

// First I make some interface that contains some structure for data
interface SomeObject { x: string, y: number, z: boolean }

// Initialize object1 with properties x, y and z, containing the important data
// I want to preserve all the data in object1 through the entire program
let object1: SomeObject = { x: "something", y: 0, z: false }
// I can print all properties of object1
console.log(`Object 1: x = ${object1.x}, y = ${object1.y}, z = ${object1.z}`) 
// OUTPUT: "Object 1: x = something, y = 0, z = false"

// omit or delete property 'x' from object 1, defining object 2
let object2 = omitMany(object1, ["x"]) // The type of object2 is: {y: number, z: boolean}
// Here I can only get properties z and y, because x has been omitted
// Calling: object2.x gives an compile error
console.log(`Object 2: y = ${object2.y}, z = ${object2.z}`)
// OUTPUT: Object 2: y = 0, z = false (as expected)

// Everything works fine from here, but...
// When I recall omitMany on object1 the following happens: 

// Initialize object3 from object1, removing 'x' from an object where x = undefined
let object3 = omitMany(object1, ["x"]) // This code compiles, omitting 'x' from object2 gives an compiler error
//Printing object3 does show no problems, since it satisfies the expected result. Remove 'x' and keep 'y' and 'z'
console.log(`Object 3: y = ${object3.y}, z = ${object3.z}`)
// OUTPUT: Object 3: y = 0, z = false

// But when I print object1 again
console.log(`Object 1: x = ${object1.x}, y = ${object1.y}, z = ${object1.z}`) 
// OUTPUT: Object 1: x = undefined, y = 0, z = false 
// We lost the data of 'x'!!!


// I also ran into problems when I try to pick property 'x' from the original object1 
let object4 = pickMany(object1, ["x"]) // The type of object4 is {x: string}
// When I print 'x' from object4 it is still undefined
console.log(`Object 4: x = ${object4.x}`) 
// OUTPUT: Object 4: x = undefined

I understand that this has to do with the behaviour of delete, but is there an other way to remove properties from an object without loosing the information of the original object? So with preserving all the values and properties.

This problem can be solved with a temporary variable, but I first wanted to see if there are other solutions.

Jason Aller
  • 3,541
  • 28
  • 38
  • 38

1 Answers1

1

This is how I solved this:

// https://stackoverflow.com/a/49579497/14357
/** Extracts optional keys from T */
export type OptionalKeys<T> = {
    [K in keyof T]-?: ({} extends {
        [P in K]: T[K];
    } ? K : never);
}[keyof T];

/** Typesafe way to delete optional properties from an object using magic of OptionalKeys<T> */
export const deleteOptionalProperty = <T>(obj: T, id: OptionalKeys<T>): T => {
    const { [id]: deleted, ...newState } = obj;
    return newState as T // this type-conversion is safe because we're sure we only deleted optional props
}

export const deleteOptionalProperties = <T>(obj: T, ...ids: OptionalKeys<T>[]): T =>
    ids.reduce((prev, id) => deleteOptionalProperty(prev, id), obj)
spender
  • 117,338
  • 33
  • 229
  • 351
  • Using `never` seems a good sollution, an property of type `never` gets almost the same treatment as a deleted property –  May 08 '19 at 11:15
  • But how do I use the this, the type `OptionalKeys` is `never`. So how am I able to give keys to the `id` parameter? –  May 08 '19 at 13:50
  • @LivingLife Gotcha. This approach is only good for deleting ***optional*** keys (i.e. `{someValue?:string}`), which allows a guarantee of being able to return the same type as the input type. If I get time later, I'll make a more permissive version. – spender May 08 '19 at 18:23
  • I have not used the `type OptionalKeys` jet, but your answer did contain some usefull parts. I refactored my `omitMany()` and `omitOne()` methods to make use of this way of removing properrties `const { [id]: deleted, ...newState } = obj;` and then return the newState. It is a clever way of removing properties, because you create a new object where the `[id]` property is set to `deleted` (the value doesn't matter) then `newState` will have all the remaining properties except for `id`. This for now solves my problem because I have an other way of removing propeties, than with `delete` keyword –  May 09 '19 at 11:34