What is the process for obtaining the form of an item and then adjusting the characteristics of each individual leaf property?

Consider this scenario:

interface SomeObject {
  prop1: number;
  prop2: string;
  prop3: {
    innerProp1: number[];
    innerProp2: string[];
    innerProp3: {
      deeperProp1: string[];
      deeperprop2: boolean;
    },
    innerProp4: {
      [key: string]: any;
    },
    innerProp5: {
      [key: string]: any;
    }
  }
}

I aim to define a type that can take the shape of any object and return the same structure with specified types for the "leaf" properties. Additionally, each property should be optional. Here is an example of what I'm looking for:

type ModifyShapeType<Shape, NewType> = ???

When applied to the interface above, it should provide type safety for the identical shape but with the desired type:

const myObject: ModifyShapeType<SomeObject, boolean> = {
  prop1: true;
  prop2: true;
  prop3: {
    // innerProp1: true;
    // innerProp2: false;
    innerProp3: {
      deeperProp1: true;
      // deeperprop2: true;
    },
    innerProp4: true,
    // innerProp5: false
  }
};

I have formulated a solution, but I want to remove the original types from the structure and substitute them with the desired types while maintaining specificity on property access. This is what I have so far:

type ModifyShapeType<S, T> = Partial<Record<keyof S, Partial<S[keyof S] | T>>>;

For a live demonstration, check out the TypeScript Playground.

Current limitations include:

  1. The types are still inferred from the original object type, resulting in mixed types.
  2. All properties now share the same type (loss of specificity on read), leading to unsafe writes as well (loss of specificity on write).

Is achieving this goal feasible?

Answer №1

It sounds like you require a complex, recursive data structure.

To achieve this, the process involves iterating through each key and determining if it is an object (branch) or another type of value (leaf). If it's a branch, recursion is necessary. If it's a leaf, the desired value type should be output. The challenge lies in defining what constitutes a leaf, as every value in JavaScript has properties and can behave somewhat like an object.

In order to accomplish this task, you'll need a conditional branch detection type and a recursive mapped type.

// Determines if T is a branch or a leaf
type IsBranch<T> = 
  T extends { [k: string]: any } ? 
    T extends any[] ? 
      never :
      T :
    never

// Recursively processes each key
// For branches, process keys and return Partial of that branch
// For leaves, replace with the specified value type
type ModifyShapeType<S, T> = S extends IsBranch<S> ?
  Partial<{ [k in keyof S]: ModifyShapeType<S[k], T> }> :
  T

const a: ModifyShapeType<{ a: number }, boolean> = { a: true }
const b: ModifyShapeType<{ a: number[] }, boolean> = { a: true }
const c: ModifyShapeType<{ a: { b: number } }, boolean> = { a: { b: true } }

Try it out in the TypeScript Playground

An interesting aspect to consider is how an array type differs from other types, requiring unique handling.

Another challenge arises when discerning between { a: number } and { [k: string]: number }. Determining whether to treat one as a branch and the other as a leaf poses difficulties, especially when considering conditional tests on T[string] for indexable properties. This distinction may prove elusive, warranting further exploration.

Answer №2

This code snippet is quite reminiscent of RecursivePartial, but with a twist. Instead of using any, it takes into account array subtypes. You can find more information on this topic here: Recursive Partial<T> in TypeScript

What sets this apart is its ability to replace leaf nodes of T with V, making it a useful tool for certain types of data manipulation without resorting to the use of any:

/**
 * Replaces leaf nodes of T with V
 */
export type RecursiveReplace<T,V> = {
  [P in keyof T]: T[P] extends (infer U)[]
    ? RecursiveReplace<U,V>[]
      : T[P] extends number | string | symbol | undefined
      ? V
      : RecursiveReplace<T[P],V>;
};

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

What is the purpose of specifying an 'any' return type in TypeScript?

As an example: function retrieveUserInformation(input: any): any { return input } It may seem unnecessary to declare that "any data type can be returned". Is there a specific reason for this? ...

Issue when trying to use both the name and value attributes in an input field simultaneously

When the attribute "name" is omitted, the value specified in "value" displays correctly. However, when I include the required "name" attribute to work with [(ngModel)], the "value" attribute stops functioning. Without using the "name" attribute, an error ...

Discovering the parameter unions in Typescript has revolutionized the way

My current interface features overloaded functions in a specific format: export interface IEvents { method(): boolean; on(name: 'eventName1', listener: (obj: SomeType) => void): void; on(name: 'eventName2', listener: (obj: Som ...

Strategies for implementing searchbar filtering in Ionic3 and Angular5 data manipulation

I'm having trouble displaying product names on my search.html page based on the search bar input. I've tried using the Ionic searchbar component but it's not working. Can anyone help me with this issue? If there's an alternative solutio ...

Using ngIf for various types of keys within a JavaScript array

concerts: [ { key: "field_concerts_time", lbl: "Date" }, { key: ["field_concert_fromtime", "field_concert_totime"], lbl: "Time", concat: "to" }, { key: "field_concerts_agereq", lbl: "Age R ...

What is the syntax for declaring a boolean or object type?

Is it possible to create a variable in TypeScript that can hold either true/false or an object of booleans? I'm still learning TS and would like some input on this syntax. variableA: { a: boolean, b: boolean } | boolean I found a workaround for now, ...

Creating a versatile function in TypeScript for performing the OR operation: A step-by-step guide

Is there a way in TypeScript to create a function that can perform an OR operation for any number of arguments passed? I currently have a function that works for 2 arguments. However, I need to make it work for any number of arguments. export const perfo ...

Error message "Could not locate module while constructing image in Docker."

I've encountered an issue while working on my nodeJS Typescript project. After successfully compiling the project locally, I attempted to create a docker image using commands like docker build or docker-compose up, but it failed with a 'Cannot fi ...

Oops! Looks like you forgot to provide a value for the form control named <name>. Make sure to fill

I have encountered an issue with a nested operation. The process involves removing an offer and then saving it again as a repeating one. However, the problem arises when I try to use patchValue() on the item in offerList[index] after saving the repeating ...

How can I retrieve query parameters in the Server app directory of Next.js 13 using a function?

I am looking to retrieve a query token from localhost/get/point?token=hello. import { NextResponse } from 'next/server' import base64url from 'base64url' type Params = { token: string } export async function GET(req: Request, contex ...

Ways to resolve the issue: The property 'X' is not recognized on the '{ object }' data type

Just getting started with vuejs and encountering an error in my vue file Issue: Property 'ClientsSrv' is not recognized on type '{ name: string; props: {}; data(): { ClientsSrv: ClientsService | null; ClientsList: ClientsModel[] | null; IsR ...

What is causing the failure of the state to be inherited by the child component in this scenario (TypeScript/React/SPFX)?

For this scenario, I have a Parent class component called Dibf and a Child class component named Header. While I can successfully pass props from the Parent to the child, I am encountering difficulties when trying to pass state down by implementing the fo ...

Stop the transmission of ts files

Recently, I noticed that when using node JS with Angular-CLI, my .ts files are being transmitted to the client through HTTP. For example, accessing http://localhost/main.ts would deliver my main.ts file to the user. My understanding is that ts files are s ...

I'm having trouble setting a value for an object with a generic type

I am attempting to set a value for the property of an object with generic typing passed into a function. The structure of the object is not known beforehand, and the function receives the property name dynamically as a string argument. TypeScript is genera ...

The type 'ClassA<{ id: number; name: string; }>' cannot be assigned to the type 'ClassA<Record<string, any>>'

One requirement I have is to limit the value type of ClassA to objects only. It should also allow users to pass their own object type as a generic type. This can be achieved using Record<string, any> or { [key: string]: any }. Everything seems to be ...

ReactJS Typescript Material UI Modular Dialog Component

Hello, I need help with creating a Reusable Material UI Modal Dialog component. It's supposed to show up whenever I click the button on any component, but for some reason, it's not displaying. Here's the code snippet: *********************TH ...

Using the reduce method in JavaScript or TypeScript to manipulate arrays

Just exploring my culture. I have grasped the concept of the reduce principle var sumAll = function(...nums: number[]):void{ var sum = nums.reduce((a, b) => a + b , 0); document.write("sum: " + sum + "<br/>"); } sumAll(1,2,3,4,5); The r ...

Issue encountered with Ionic and ssh2: process.binding is not supported

I am currently delving into the world of Ionic and experimenting with creating a basic application that utilizes SSH2 to establish an ssh connection between the app and a server. Here is a breakdown of the steps I took to encounter the issue: Steps to Rep ...

What is the best method for transitioning to a new page in React Native using Ignite Bowser?

Recently I ventured into the world of React Native with Ignite Bowser. My current project involves building a React Native app using Ignite Bowser. At the start of my app, there's a welcoming screen that pops up. It features a 'continue' bu ...

Exploring the traversal of an array of objects within Tree Node

How can I transform my data into a specific Tree Node format? Is there a method (using Typescript or jQuery) to iterate through each object and its nested children, grandchildren, and so on, and modify the structure? Current data format { "content" ...