What are the steps to create custom Typescript RecursiveOmit and RecursivePick declarations for efficient cloning routines?

For some time now, I have been attempting to create a declaration for RecursiveOmit and RecursivePick in cloning methods such as 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've experimented with different variations of this code and explored similar answers from other questions. However, despite several attempts, I have not been able to make it work.

If anyone has any insights on what might be going wrong, please share!

Answer №1

It appears to me that there may be a bug in TypeScript. After conducting an investigation on GitHub, I did not come across any relevant issues related to this particular issue. However, I have identified a workaround that seems to solve the problem. The solution involves cleaning up the type definitions with key remapping and utilizing Extract/Exclude:

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

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

Next, the functions can be modified using 5.0 const generics:

const replicate = <T, const K extends SelectiveKey<T>>(
    item: T,
    authorizedProperties: K[]
): SelectivePick<T, K> => {
    return JSON.parse(JSON.stringify(item, authorizedProperties as any));
};

const replicateWithBlackList = <T, const K extends SelectiveKey<T>>(
    item: T,
    restrictedProperties: K[]
): SelectiveOmit<T, K> => {
    return JSON.parse(
        JSON.stringify(item, (key: any, value: any): any =>
            restrictedProperties.includes(key) ? undefined : value
        )
    );
};

Interactive Workspace

Answer №2

Upon examining the function call with a hover, it becomes clear that the issue lies in K being determined as

"a" | "b" | "c"
(specifically RecursiveKey<typeof c>) rather than just "b".

This can be rectified by explicitly stating (

clone<typeof c, "b">(c, ["b"])
or clone(c, ['b' as const])), but also note that TypeScript 5.0 introduces a solution with 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> => {…}

(playground updated)

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

Obscured painting surface appearance using Three.js

I am attempting to incorporate a blurred texture into my Three.js scene, but the results are not what I expected. Canvas: var c = document.getElementById("myCanvas"); var context1 = c.getContext("2d"); context1.filter = "blur(16px)"; context1.beginPath( ...

Encountering Typescript issues while trying to access @angular/core packages

Recently, I made an update to my Ionic app from Angular 7 to Angular 8, and an odd error popped up: The issue lies in the fact that I am unable to access any of the standard classes stored in the @angular/core module. This same problem is occurring with o ...

Issue: encountered a write EPIPE error while attempting to transfer a file to FTP via gulp

Whenever I try to deploy some stylesheets to a server using FTP, I encounter an error about 80% of the time. Here's the specific error message: Error: write EPIPE at _errnoException (util.js:1022:11) at WriteWrap.afterWrite [as oncomplete] (net.j ...

Node.js: Attempting to arrange ISO dates based on their time span

I am currently working on creating a line chart using the chart.js library. In order to do this, I have an array that contains ISO dates and my goal is to determine which ISO dates belong to the same hour and day. This information will then be used to gene ...

The controller in Angular uses the $scope symbol _

App.controller('todoController', function ($scope) { // create a message to display in our view $scope.todos = [{ name: 'angular', done: false }]; $scope.clearTodo = function () { $scope.todos = _.filter($scope.tod ...

Display loading spinner and lock the page while a request is being processed via AJAX

Currently, I am working on a project using MVC in C#, along with Bootstrap and FontAwesome. The main objective of my project is to display a spinner and disable the page while waiting for an ajax request. Up until now, I have been able to achieve this go ...

Can you rely on a specific order when gathering reactions in a discord.js bot?

Imagine a scenario where a bot is collecting reactions to represent event registrations. To prevent any potential race conditions, I have secured the underlying data structure with a mutex. However, the issue of priority still remains unresolved as user # ...

I am experiencing issues with the middleware not functioning properly after implementing a custom application with Next.js

I'm currently diving into Next.js version 13 and attempting to customize the app based on the standard documentation. However, it seems that the middleware isn't being invoked as expected. I suspect there might be something wrong with my implemen ...

Exploring the Concept of Template Element Recursion in Angular JS 2

In my Angular 2 project, I encountered a situation where I needed to iterate through ngFor based on child elements. My component should be able to render a list based on the input provided. Here is an example of the data structure: [ { name: 'ABC ...

In React, I am currently working on implementing a feature that will allow the scene camera to move as the user scrolls

I'm currently working on incorporating a feature to enable movement of the scene camera when scrolling in React. However, as I am relatively new to using this library, I am unsure which implementation approach would be most suitable for achieving this ...

removing duplicate items from an array in Vue.js

I created a Pokemon App where users can add Pokemon to their 'pokedex'. The app takes a pokemon object from this.$store.state.pokemon and adds it to this.$store.state.pokedex. However, users can add the same pokemon multiple times to the pokedex, ...

Is it possible to direct the user to a particular link upon swiping to unlock?

Hello everyone, I could use some assistance with this code. I am looking to redirect someone to a specific page when they swipe to unlock and automatically transfer them to another page when the swipe ends. Any help is greatly appreciated. Thank you! $ ...

What is the process for cancelling an interval when it is disabled in my configuration file?

To automate a bot, I want it to stop running an interval if the configuration file specifies "off" and continue running if it says "on". I attempted this: Using discord.js: config.Interval = setInterval(() => { WallCheck.send(WallCheckemb ...

Ensuring draggable div remains fixed while scrolling through the page

I am currently working on creating a draggable menu for my website. The menu is functional and can be moved around the page as intended. However, I have encountered an issue where the menu disappears when scrolling down the page due to its position attrib ...

Drag and Drop Feature using Angular with JQuery UI for Cloning Elements

I've been working on a web design project where I want to create a draggable user interface. https://i.sstatic.net/Be0Jk.png The goal is for users to click and drag different types of questions from the left side to the right side of the screen. Cu ...

Before beginning my selenium scripts, I need to figure out how to set an item using Ruby in the browser's localStorage

Before running my selenium scripts, I am attempting to store an item in the browser's localStorage. I attempted to clear the local storage using this command: driver.get('javascript:localStorage.clear();') This successfully cleared the lo ...

Guide to verifying current data using the jQuery validation library combined with CodeIgniter 4 in the presence of automatic CSRF protection

I am currently working on validating a form using the jQuery validation plugin and CodeIgniter 4. I have enabled CSRF protection that auto generates for each request. Initially, I can successfully validate the form on the first request. However, on subsequ ...

The JSX component cannot be utilized as `ToastContainer`

Check out this Code: import axios from "axios"; import React, { useState, useEffect } from "react"; import { ToastContainer, toast } from "react-toastify"; import loaderIcon from "../../assets/images/loader.gif"; imp ...

Reorganizing asynchronous code into a nested synchronous structure in Meteor/React

Recently, I've been experimenting with running data fetching code in a container component and passing it to the display component in order to streamline my use of hooks and reduce load time. I've tested both await/sync and Meteor's wrapAsyn ...

Is it feasible to obtain the userId or userInfo from the Firebase authentication API without requiring a login?

Is it feasible to retrieve the user id from Firebase authentication API "email/password method" without logging in? Imagine a function that takes an email as a parameter and returns the firebase userId. getId(email){ //this is just an example return t ...