Secure TypeScript Omit Utility for Ensuring Type Safety

I need to create a custom implementation of Lodash's _.omit function using plain TypeScript. The goal is for the omit function to return an object with specific properties removed, which are specified as parameters after the initial object parameter.

Here is my current attempt:

function omit<T extends object, K extends keyof T>(obj: T, ...keys: K[]): {[k in Exclude<keyof T, K>]: T[k]} {
    let ret: any = {};
    let key: keyof T;
    for (key in obj) {
        if (!(keys.includes(key))) {
            ret[key] = obj[key];
        }
    }
    return ret;
}

This code results in the following error message:

Argument of type 'keyof T' is not assignable to parameter of type 'K'.
  Type 'string | number | symbol' is not assignable to type 'K'.
    Type 'string' is not assignable to type 'K'.ts(2345)
let key: keyof T

Based on this error, I believe:

  1. The variable key is a keyof T, and since T is an object, key can be a symbol, number, or string.

  2. When using the for in loop, key is restricted to being a string, while the includes method could potentially handle a number when passed an array, leading to a type mismatch.

Any suggestions on why this approach is flawed and how it can be rectified would be greatly appreciated!

Answer №1

interface OmitKeys {
    <T extends object, K extends [...(keyof T)[]]>
    (obj: T, ...keys: K): {
        [K2 in Exclude<keyof T, K[number]>]: T[K2]
    }
}

const omitKeys: OmitKeys = (obj, ...keys) => {
    const result = {} as {
        [Key in keyof typeof obj]: (typeof obj)[Key]
    };
    let key: keyof typeof obj;
    for (key in obj) {
        if (!(keys.includes(key))) {
            result[key] = obj[key];
        }
    }
    return result;
};

To make things easier, I've transferred most of the typifications to an interface.

The issue was that K had been wrongly inferred as a tuple, instead of a union of keys. So, I adjusted its type constraint as follows:

[...(keyof T)[]] // can be unpacked as:
keyof T // a union of keys from T
(keyof T)[] // an array holding keys from T
[...X] // a tuple containing X (zero or more arrays similar to the one described above)

Next, we needed to convert the tuple K into a union (to Exclude it from keyof T). This is achieved through K[number], which basically means what it says - turning

T[keyof T]</code into a union of values from <code>T
.

Playground

Answer №2

Easiest method:

export const filterOut = <T extends object, K extends keyof T>(obj: T, ...keys: K[]): Exclude<T, K> => {
  keys.forEach((key) => delete obj[key])
  return obj
}

Transformed into a pure function:

export const filterOut = <T extends object, K extends keyof T>(obj: T, ...keys: K[]): Exclude<T, K> => {
  const _ = { ...obj }
  keys.forEach((key) => delete _[key])
  return _
}

Answer №3

The answer provided by Nurbol earlier may be a popular choice, but I have my own approach implemented in the utils-min program.

This solution involves utilizing TypeScript's built-in Omit feature and is specifically tailored to only handle string key names. (Although there is still room for improvement with regards to handling Set to Set conversions, everything else seems to be functioning smoothly)

export function omit<T extends object, K extends Extract<keyof T, string>>(obj: T, ...keys: K[]): Omit<T, K> {
  let result: any = {};
  const exclusionSet: Set<string> = new Set(keys); 
  // Note: Setting type as Set<K> causes issues with checking obj[key]'s type.

  for (let key in obj) {
    if (!exclusionSet.has(key)) {
      result[key] = obj[key];
    }
  }
  return result;
}

Answer №4

Object.keys and for in methods retrieve keys as strings, excluding symbols. Numeric keys are automatically converted to strings.

If you have numeric string keys in an object, you must convert them to numbers to avoid returning the object with string keys.

function omit<T extends Record<string | number, T['']>,
 K extends [...(keyof T)[]]>(
    obj: T,
    ...keys: K
): { [P in Exclude<keyof T, K[number]>]: T[P] } {
    return (Object.keys(obj)
         .map((key) => convertToNumbers(keys, key)) as Array<keyof T>)
        .filter((key) => !keys.includes(key))
        .reduce((agg, key) => ({ ...agg, [key]: obj[key] }), {}) as {
        [P in Exclude<keyof T, K[number]>]: T[P];
    };
}

function convertToNumbers(
    keys: Array<string | number | symbol>,
    value: string | number
): number | string {
    if (!isNaN(Number(value)) && keys.some((v) => v === Number(value))) {
        return Number(value);
    }

    return value;
}


// omit({1:1,2:'2'}, 1) will return {'1':1, '2':'2'} if convertToNumbers function is not used.
// Using a numeric string instead of a number will result in failure within Typescript

To include symbols in your object manipulation code, you can utilize the following snippet:

function omit<T, K extends [...(keyof T)[]]>(
    obj: T,
    ...keys: K
): { [P in Exclude<keyof T, K[number]>]: T[P] } {
    return (Object.getOwnPropertySymbols(obj) as Array<keyof T>)
        .concat(Object.keys(obj)
        .map((key) => convertToNumbers(keys, key)) as Array<keyof T>)
        .filter((key) => !keys.includes(key))
        .reduce((agg, key) => ({ ...agg, [key]: obj[key] }), {}) as {
        [P in Exclude<keyof T, K[number]>]: T[P];
    };
}

Answer №5

While facing a similar issue where I needed to make sure there were no typos when omitting properties, I found a solution that worked for me:

export interface Person {
  id: string;
  firstName: string;
  lastName: string;
  password: string;
}

type LimitedDTO<K extends keyof Person> = Omit<Person, K>;

export type PersonDTO = LimitedDTO<"password" | "lastName">;

By using this approach, TypeScript will prevent you from omitting a property that is not present in the Person interface.

Answer №6

When we restrict the type of keys to string [], it functions correctly. However, this approach may not be the most optimal. It would be better if keys are allowed to be string | number | symbol[];

function excludeKeys<T, K extends string>(
  obj: T,
  ...keys: K[]
): { [k in Exclude<keyof T, K>]: T[k] } {
  let result: any = {};
  Object.keys(obj)
    .filter((key: K) => !keys.includes(key))
    .forEach(key => {
      result[key] = obj[key];
    });
  return result;
}
const output = excludeKeys({ a: 1, b: 2, c: 3 }, 'a', 'c');
// The compiler deduced output as 
// {
//   b: number;
// }

Answer №7

Regrettably, eliminating as any is an unattainable task.

const removeProperty = <Obj, Prop extends keyof Obj>(
  obj: Obj,
  prop: Prop
): Omit<Obj, Prop> => {
  const { [prop]: _, ...rest } = obj;

  return rest;
};

export default removeProperty;


const omit = <Obj, Prop extends keyof Obj, Props extends ReadonlyArray<Prop>>(
  obj: Obj,
  props: readonly [...Props]
): Omit<Obj, Props[number]> =>
  props.reduce(removeProperty, obj as any);

Playground

Answer №8

Utilizing the array reduce function to eliminate certain properties.

const filterProperties = <T extends object, K extends keyof T>(
  data: T,
  props: Array<K>
): Omit<T, K> => {
  if (!data || !Array.isArray(props) || !props.length) {
    return data;
  }
  return props.reduce((acc, prop) => {
    const { [prop as keyof object]: prop1, ...rest } = acc;
    return rest;
  }, data);
};

Answer №9

Try out this concise one-liner that creates a new object by utilizing the function Object.fromEntries():

const omitProperties = <T extends Record<string, unknown>, K extends keyof T>(obj: T, ...keys: K[]): Omit<T, K> => {
  return Object.fromEntries(Object.entries(obj).filter(([key]) => !keys.includes(key as K))) as Omit<T, K>
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is it possible to send an email with an attachment that was generated as a blob within the browser?

Is there a way to attach a file created in the browser as a blob to an email, similar to embedding its direct path in the url for a local file? The file is generated as part of some javascript code that I am running. Thank you in advance! ...

Can React-Select be utilized in the browser directly from the CDN?

Is it possible to utilize react-select directly in the browser without using bundlers nowadays? The most recent version that I could find which supports this is 2.1.2: How to import from React-Select CDN with React and Babel? In the past, they provided r ...

Updating values within an ng-template can be achieved by accessing and modifying the

I have come across this specific template <script type="text/ng-template" id="template"> <span class="count-container"> <span >{{count}}</span> </span> </script> and it is being included multiple times ...

"Implementing a call and waiting at intervals by utilizing the subscribe function in Angular 6

In my code, I have a method that is called every 10000 times. Now, I want to modify this so that the function getAllNotificationsActed0() is invoked every 10 seconds. If the data does not arrive within this interval, I do not want the function to be called ...

What is the best way to retrieve a value from an asynchronous function in Node.js?

var fs = require('fs'); var ytdl = require('ytdl-core'); var favicon = require('serve-favicon'); var express = require('express'); var app = express(); app.use(favicon(__dirname + '/public/favicon.png')); ...

Tips for ensuring a file has downloaded correctly

For my current project, I have a requirement to download a file which should be automatically deleted after being successfully downloaded. To ensure that the file is completely downloaded before proceeding with deletion, I initially set async:false in the ...

Adjust the width of a div element using the transform property to rotate it and modify the

Having an issue with a particular style here. There's a div containing the string "Historic" that has a CSS transform rotate applied to it and is positioned in relation to another sibling div. When I change the string to "Historique" for international ...

What could be causing the node inspector to fail to launch when using nodemon and ts-node together?

I have a basic node server set up in typescript. The configuration in my package.json file looks like this: "scripts": { "build": "tsc", "dev": "nodemon --watch src/**/* -e ts,json --exec ts-node ./src/server.ts", "debug": "nodemon --verbose --wat ...

MongoDB date query with $gte and $le operators mm/dd/yy

Apologies in advance for any language errors. The problem I am dealing with involves a column in the database called "Date" inside the "startOn" Object. This setup creates a structure like "startOn.Date" with data format as yyyy/dd/mm. For example, if we ...

What is the best way to uninstall yarn from angular cli so that I can switch to using npm for installing packages?

When attempting to install packages using yarn for tooling, an error occurred stating: 'yarn' is not recognized as an internal or external command, operable program or batch file. Package installation failed with the details provided above. For f ...

Update database upon drag-and-drop using jQuery

I have a dataset (shown below) that undergoes sorting by 'section'. Each item is placed into a UL based on its section when the page loads. My goal is to automatically update the section in the database when a user drags an item to a different s ...

Issue with the AngularJS build in Yeoman

After setting up Yeoman and Bootstrap for an AngularJS project, I utilized the grunt server command for debugging and coding. However, when I tried using the 'grunt build' command, it generated a dist folder. Unfortunately, when I attempted to ru ...

Create a typescript class object

My journey with Typescript is just beginning as I delve into using it alongside Ionic. Coming from a background in Java, I'm finding the syntax and approach quite different and challenging. One area that's giving me trouble is creating new object ...

Failure to register Express Route

I am currently using express and facing some challenges with creating routes using express.Router. Below is my index.js file (npm main file): require('dotenv').config() const express = require('express') const loaders = require('. ...

What is the best way to access and iterate through JSON data?

I am trying to extract data from my Json file, however, I cannot use a specific 'key' because it changes on a daily basis. https://i.sstatic.net/sZySk.png My attempted solution is as follows: template: function(params) { const objects ...

How can the data from a factory be utilized within the same controller but in a separate file in an AngularJS application?

When trying to route from home.html to profile.html, there seems to be an issue with the execution of the profile.js script. I have data in script.js that is written in a factory. When the profile button is clicked, the page routes to profile.html, but the ...

Resolve issues with vue js checkbox filtering

As I embark on my Vue journey, I came across some insightful queries like this one regarding filtering with the help of computed(). While I believe I'm moving in the right direction to tackle my issue, the solution has been elusive so far. On my web ...

Converting city/country combinations to timezones using Node.js: A comprehensive guide

When provided with the name of a city and country, what is the most reliable method for determining its timezone? ...

Ways to prevent a div from loading an HTML page

When an external button is clicked, the remove() function on id main is triggered. The issue arises when a user quickly clicks on btn1 and then presses the external button, causing the removal to occur before the event handler for btn1. This results in the ...

Having trouble retrieving data from the MongoDB database using Node.js

Having trouble with data retrieval from MongoDb Successfully connected to MongoDb, but when using the find command, it should return an empty collection, yet nothing is being returned. What could be causing this issue and how can it be monitored through ...