Designations for removing an item at a targeted subdirectory location

type Taillet<T extends any[]> = ((...t: T) => void) extends ((
  h: any,
  ...r: infer R
) => void)
  ? R
  : never;

type NestedOmit<T, Path extends string[]> = T extends object
  ? {
      0: Omit<T, Path[0]>;
      1: {
        [K in keyof T]: K extends Path[0] ? NestedOmit<T[K], Taillet<Path>> : T[K];
      };
    }[Path['length'] extends 1 ? 0 : 1]
  : T;

The defined type above functions properly. I have validated it using the following method:

type Test = NestedOmit<{ a: { b: { c: 1 } } }, ['a', 'b', 'c']>;

// {
//    a: {
//        b: Pick<{
//            c: 1;
//        }, never>;
//    };
// }

However, when implementing this within a function, the expected result is not obtained.

const remove = <T extends object, K extends string[]>(src: T, path: K) => {
  // custom logic omitted. only constructing the result below.
  const outcome = {
    a: {
      b: {}
    }
  }
  return result as NestedOmit<T, K>
};

const modifiedObject = remove({a:{b:{c:1}}},["a","b","c"]);
// modifiedObject.a.b.c <========== still works
// desired output is modifiedObject.a.b

Could anyone identify the issue in this scenario?

Answer №1

The issue you're encountering is due to the compiler's tendency to transform tuples into arrays and literals into nonliterals unless specific hints are provided. The caller of del() has the option to utilize an as const assertion on the path parameter in order to achieve this effect (although it requires accepting readonly string[] instead of just string[]), but ideally, the caller shouldn't have to worry about this. I've submitted a request on microsoft/TypeScript#30680 for something like as const on the call signature to handle this. Without such a feature, using unconventional hints becomes necessary.

If you want an array type to be inferred as a tuple whenever possible, it's advisable to include a tuple type in its context, such as within a union. Thus, T extends Foo[] would become T extends Foo[] | []. Similarly, if you prefer a type to be inferred as a string literal when feasible, incorporating a string literal or another hint resembling a string is recommended. As a result, T extends string[] can be modified to

T extends (string | (""&{__:0}))[]
, or preferably, T extends N[] where N serves as another generic parameter defined as N extends string. Here's how it looks:

declare const del: <T extends object, K extends N[] | [], N extends string>(
    src: T, path: K
) => DeepOmit<T, K>;

You can observe it functioning as intended:

const deletedObj = del({ a: { b: { c: 1 } } }, ["a", "b", "c"]);
deletedObj.a.b; // valid
deletedObj.a.b.c; // error

Furthermore, it's worth noting that the implementation of your DeepOmit could be simplified; the technique involving

{0: X, 1: Y}[T extends U ? 0 : 1]
is used to bypass circular errors with T extends U ? X : Y, even though this workaround is not officially supported. However, creating DeepOmit directly does not violate these rules: a recursive type with recursion occurring within an object property is acceptable:

type DeepOmit<T, Path extends string[]> = T extends object ?
    Path['length'] extends 1 ? Omit<T, Path[0]> : {
        [K in keyof T]: K extends Path[0] ? DeepOmit<T[K], Tail<Path>> : T[K];
    } : T; // no error, still works

Hopefully, this information proves helpful; best of luck!

Link to Playground Code

Answer №2

The issue arises from the type inference of the second argument. While the type example is defined as strict like ['a','b','c'], during function execution, the argument is inferred as string[]. This results in your type level function DeepOmit being called with string[] instead of ['a','b','c']. The outcome remains the same:

type Result = DeepOmit<{ a: { b: { c: 1 } } }, string[]>;

To resolve this issue, we can either strictly type the variable by specifying ['a','b','c'] as ['a','b','c'], which can be verbose, or enhance the function to treat each argument separately (using spread syntax) for stricter checking. While this may slightly alter the function's API (possibly for the better 👍), it ensures that inference works as intended. Here's an example:

const del = <T extends object, K extends string[]>(src: T, ...path: K) => {
  // custom logic removed. just creating the result below.
  const result = {
    a: {
      b: {}
    }
  }
  return result as DeepOmit<T, K>
};

const deletedObj = del({ a: { b: { c: 1 } } }, 'a', 'b', 'c');
deletedObj.a.b.c // error no c !

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

Determine the type of a function to assign to the parent object's property

Consider the following scenario: class SomeClass { public someProperty; public someMethodA(): void { this.someProperty = this.someMethodB() } public someMethodB() { ...some code... } } I need the type of somePropert ...

Troubleshooting the creation of migration paths in NestJS with TypeORM

After diligently studying the NestJS and TypeORM documentation, I have reached a point where I need to start generating migrations. While the migration itself is creating the correct queries, it is not being generated in the desired location. Currently, m ...

Some files are missing when performing an npm install for a local package

My project is structured like this: ├── functions/ │ ├── src │ ├── lib │ ├── package.json ├── shared/ │ ├── src │ | ├── index.ts | | ├── interfaces.ts | | └── validator_cl ...

What is preventing my function from retrieving user input from an ngForm?

I'm currently working on my angular component's class. I am attempting to gather user input from a form and create an array of words from that input. The "data" parameter in my submit function is the 'value' attribute of the ngForm. Fo ...

Is it possible for TypeScript to automatically determine the type of an imported module based on its path?

I'm currently working on creating a function, test.isolated(), which wraps around jest.isolateModules. This function takes an array of strings representing the modules to be imported, along with the usual arguments (name, fn, timeout), and then inject ...

Troubleshooting React/Jest issues with updating values in Select elements on onChange event

I am currently testing the Select element's value after it has been changed with the following code: it("changes value after selecting another field", () => { doSetupWork(); let field = screen.getByLabelText("MySelectField") ...

Attempting to utilize the setInterval function in Ionic 4 to invoke a specific function every second, unfortunately, the function fails to execute

Working with Ionic 4 has been a breeze for me. Recently, I encountered a situation where I needed to update the value of an ion-range every second by invoking a function. However, despite successfully compiling the code, the changeMark function never seeme ...

Commit to choosing an option from a dropdown menu using TypeScript

I just started learning about typescript and I have been trying to create a promise that will select options from a drop down based on text input. However, my current approach doesn't seem to be working as expected: case 'SelectFromList': ...

Create a series of actions that do not depend on using only one occurrence of the WriteBatch class

My goal is to create a series of batch actions using functions that do not require a specific instance of WriteBatch. Currently, I am passing an instance of the WriteBatch class to the functions so they can utilize the .set(), .update(), or .delete() metho ...

What is the best way to hand off this object to the concatMap mapping function?

I'm currently in the process of developing a custom Angular2 module specifically designed for caching images. Within this module, I am utilizing a provider service that returns Observables of loaded resources - either synchronously if they are already ...

Efficiently search and filter items across multiple tabs using a single search bar in the Ionic 2

I am currently working on implementing a single search bar that can filter lists in 2 different tabs within Ionic 2. The search bar is functional, and I have a method for filtering through objects. However, my goal is to allow users to select different tab ...

"Perform an upsert operation with TypeORM to create a new entry if it

Is there a built-in feature in TypeORM to handle this scenario efficiently? let contraption = await thingRepository.findOne({ name : "Contraption" }); if(!contraption) // Create if not exist { let newThing = new Thing(); newThing.name = "Contrapt ...

Learn how to enhance a Vue component by adding extra properties while having Typescript support in Vue 3

Attempting to enhance a Vue component from PrimeVue, I aim to introduce extra props while preserving the original components' typings and merging them with my new props. For example, when dealing with the Button component that requires "label" to be ...

Do TypeScript project references provide value when noEmit is used?

To enhance the speed of my editor interaction and reduce the time taken by tsc to run on my TypeScript code, I am considering implementing project references. Many teams have reported substantial performance improvements after incorporating project referen ...

Enabling non-declarative namespaces in React using Typescript: A beginner's guide

I'm diving into the React environment integrated with Typescript, but I still have some confusion about its inner workings. I really hope to receive thorough answers that don't skip any important details. I came across a solution that involves d ...

Having trouble with Vue i18n and TypeScript: "The '$t' property is not recognized on the 'VueConstructor' type." Any suggestions on how to resolve this issue?

Within my project, some common functions are stored in separate .ts files. Is there a way to incorporate i18n in these cases? // for i18n import Vue from 'vue' declare module 'vue/types/vue' { interface VueConstructor { $t: an ...

Why is the return type for the always true conditional not passing the type check in this scenario?

Upon examination, type B = { foo: string; bar: number; }; function get<F extends B, K extends keyof B>(f: F, k: K): F[K] { return f[k]; } It seems like a similar concept is expressed in a different way in the following code snippet: functi ...

Angular firebase Error: The parameter 'result' is missing a specified type and is implicitly assigned the 'any' type

I have encountered an issue with the code I am working on and both the result and error are throwing errors: ERROR in src/app/login/phone/phone.component.ts(48,75): error TS7006: Parameter 'result' implicitly has an 'any' type. s ...

Enabling or disabling a button dynamically in Ionic based on a conditional statement

I am looking to dynamically enable or disable buttons based on specific conditions. The data is retrieved from Firebase, and depending on the data, I will either enable or disable the button. For example, if a user passes the First Quiz, I want to enable ...

Transforming an ordinary JavaScript object into a class instance

As I was delving into Angular's documentation on "Interacting with backend services using HTTP", I came across the following statement in the "Requesting a typed response" section: ...because the response is a plain object that cannot be automatical ...