Flatten an object in TypeScript

Can the structure of this type be flattened?

type MySchema = {
  fields: {
    hello: {
      type: 'Group'
      fields: {
        world: {
          type: 'Group'
          fields: { yay: { type: 'Boolean' } }
        }
      }
    }
    world: { type: 'Boolean' }
  }
}

Transforming it into:

type MyFlattenedSchema = {
  hello: { type: 'Group' }
  'hello.world': { type: 'Group' }
  'hello.world.yay': { type: 'Boolean' }
  world: { type: 'Boolean' }
}

I've been attempting to solve this for a couple of days now, but all I'm getting is a flattened union:

type FSchema = { type: string; fields?: Record<string, FSchema> }

type GetPathAndChilds<
  T extends Record<string, FSchema>,
  PK extends string | null = null
> = {
  [FN in keyof T & string]-?: {
    path: PK extends string ? `${PK}.${FN}` : `${FN}`
    type: T[FN]['type']
    // config: T[K]
    childs: T[FN] extends { fields: Record<string, FSchema> }
      ? GetPathAndChilds<
          T[FN]['fields'],
          PK extends string ? `${PK}.${FN}` : `${FN}`
        >
      : never
  }
}[keyof T & string]

type FlattenToUnion<T extends { path: string; type: string; childs: any }> =
  T extends {
    path: infer P
    type: infer U
    childs: never
  }
    ? { [K in P & string]: { type: U } }
    : T extends { path: infer P; type: infer U; childs: infer C }
    ? { [K in P & string]: { type: U } } | FlattenToUnion<C>
    : never

type MySchemaToUnion = FlattenToUnion<GetPathAndChilds<TestSchema['fields']>>
//   | { hello: { type: 'Group' } }
//   | { 'hello.world': { type: 'Group' } }
//   | { 'hello.world.yay': { type: 'Boolean' } }
//   | { world: { type: 'Boolean' } }

After researching on stackoverflow, the error 'Type instantiation is excessively deep and possibly infinite' is what I keep encountering.

Answer №1

From my experience, handling deeply nested type operations always brings about unexpected challenges with various edge cases such as optional properties, union types, or index signatures. The behavior in such cases is often unpredictable, requiring thorough testing against anticipated scenarios. Be prepared for the need to refactor if something unexpectedly goes wrong despite a seemingly perfect solution.

With that said, I would propose an implementation called FlattenSchema<> that aims to transform your example input into the desired output. However, its usefulness in actual production code may vary.


The goal is to create a function FlattenSchema<T> that takes an object T with a property named fields of type

object</code and flattens these fields. Here's how we can achieve this:</p>
<pre><code>type FlattenSchema<T extends { fields: object }> =
   FlattenFields<T["fields"]>;

Next, we define FlattenFields<T>, where T represents an object type whose properties themselves contain either a type property (to be directly outputted) or a fields property (for recursion using FlattenSchema) or both:

type FlattenFields<T extends object> = { [K in keyof T]: (x:
   (T[K] extends { type: any } ?
      Record<K, { type: T[K]['type'] }>
      : unknown) &
   (T[K] extends { fields: object } ?
      PrependKey<K, FlattenSchema<T[K]>>
      : unknown)
) => void }[keyof T] extends (x: infer I) => void ?
   { [K in keyof I]: I[K] } : never;

This entails multiple steps, including intersecting the results for each property of

T</code, leveraging TypeScript features to techniques like union-to-intersection conversions when needed.</p>
<p>If <code>T[K]
contains a type property, we utilize Record utility type to output it. If it includes a fields property, we resort to recursing FlattenSchema. We then combine these outputs cautiously to avoid cluttered results through mapped types.

Lastly, to concatenate keys appropriately, we have PrependKey:

type PrependKey<K extends PropertyKey, T> =
   { [P in Exclude<keyof T, symbol> as
      `${Exclude<K, symbol>}.${P}`]: T[P] };

This involves key remapping along with template literal types to add prefixes seamlessly.


Testing our implementation:

type MySchema = {
   fields: {
      hello: {
         type: 'Group'
         fields: {
            world: {
               type: 'Group'
               fields: { yay: { type: 'Boolean' } }
            }
         }
      }
      world: { type: 'Boolean' }
   }
}
   
type MyFlattenedSchema = FlattenSchema<MySchema>;
/* Output compared to original Schema:
    hello: { type: "Group"; };
    "hello.world": { type: "Group"; };
    "hello.world.yay": { type: "Boolean"; };
    world: { type: "Boolean"; };
} */

The flattened schema aligns perfectly with the intended structure! (Remember the initial caution though!)

Try the code on TypeScript Playground

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

The combination of Next.JS and React Objects is not acceptable as a React child

Summary: Encountering the error Error: Objects are not valid as a React child (found: [object Promise]) while making a fetch request in a Typescript project. Interestingly, the same code snippet works without errors in a Javascript project. Recently, I ...

setting the minimum date for a datepicker

Does anyone know how to set the minimum date for a calendar to be 2 days from the current date? For example, if today is the 27th, the minimum date should be the 29th. Any suggestions? Thanks. https://i.sstatic.net/7yHhH.png #html code <mat-form-field ...

Navigating back to the previous page: Implementing the Router Module in Ionic 4 with Angular

One of the features on my application involves a camera page that is accessed by other pages. This camera page includes functions related to the camera preview (camera.ts): // camera.ts import { Component, OnInit } from '@angular/core'; import ...

Guide to creating two-way data binding using ngModel for custom input elements like radio buttons

I am currently facing an issue with implementing a custom radio button element in Angular. Below is the code snippet for the markup I want to make functional within the parent component: <form> <my-radio [(ngModel)]="radioBoundProperty" value= ...

Encountering an issue with Nuxt 3.5.1 during the build process: "ERROR Cannot read properties of undefined (reading 'sys') (x4)"

I am currently working on an application built with Nuxt version 3.5.1. Here is a snippet of the script code: <script lang="ts" setup> import { IProduct } from './types'; const p = defineProps<IProduct>(); < ...

Guide for referencing brackets with Joi validation

{ "visibleFields": { "design.content.buttons.action.type": { "SHOW_CLOSE": true, "URL": true, "CALL_PHONE": true }, "design.content.formFields": false, "success": fal ...

Creating a primary index file as part of the package building process in a node environment

Currently, I have a software package that creates the following directory structure: package_name -- README.md -- package.json ---- /dist ---- /node_modules Unfortunately, this package cannot be used by consumers because it lacks an index.js file in the r ...

Customize buttons in Material UI using the styled component API provided by MUI

I am intrigued by the Material UI Styled Component API, not to be confused with the styled-component library. However, I am facing difficulty in converting my simple button component into a linked button. Can anyone advise me on how to incorporate a react ...

The entire DOM in Angular2+ flickers upon loading a component using ngFor

I am facing an issue where, after a user clicks on an item to add it to a list and then renders the list using ngFor, there is a flickering effect on the screen/DOM. The strange thing is that this flicker only happens after adding the first item; subsequen ...

Exploring the Potential of Jest Testing for Angular 6 Services

Hey there, I seem to be facing a bit of a roadblock and could use some assistance. Here's the situation - I'm trying to test a service using Jest, but all the tests pass without any issues even when they shouldn't. Here are the details of t ...

Guide on accessing the afterClosed() method / observable in Angular from a Modal Wrapper Service

Currently, I am in the process of teaching myself coding and Angular by developing a personal app. Within my app, I have created a wrapper service for the Angular Material ModalDialog. It's a mix of Angular and AngularJS that I've been working on ...

Extracting the content within Angular component tags

I'm looking for a way to extract the content from within my component call. Is there a method to achieve this? <my-component>get what is here inside in my-component</my-component> <my-select [list]="LMObjects" [multiple]=&qu ...

Using TypeScript, implement a function that is called when a React checkbox's state changes to true

Currently, I am experimenting with the react feature called onChange. My goal is to update local data by adding a value when a checkbox is selected. Conversely, when the checkbox is unselected, I just want to display the original data. However, I find that ...

Ways to specify a setter for a current object property in JavaScript

Looking to define a setter for an existing object property in JavaScript ES6? Currently, the value is directly assigned as true, but I'm interested in achieving the same using a setter. Here's a snippet of HTML: <form #Form="ngForm" novalida ...

Is there a way to incorporate an "else" condition in a TypeScript implementation?

I am trying to add a condition for when there are no references, I want to display the message no data is available. Currently, I am working with ReactJS and TypeScript. How can I implement this check? <div className="overview-text"> < ...

Tips for solving a deliberate circular dependency in an angular provider

If the existing injection token for this provider is available, I want to use it. Otherwise, I will use the specified provider. Below is the code snippet: providers: [ { provide: DesignerRecoveryComponentStore, useFacto ...

Strange occurrences observed in the functionality of Angular Material Version 16

Encountered a strange bug recently. Whenever the page height exceeds the viewport due to mat-form-fields, I'm facing an issue where some elements, particularly those from Angular Material, fail to load. Here's a GIF demonstrating the problem: GI ...

Retrieve all services within a Fargate Cluster using AWS CDK

Is there a way to retrieve all Services using the Cluster construct in AWS CDK (example in TypeScript but any language)? Here is an example: import { Cluster, FargateService } from '@aws-cdk/aws-ecs'; private updateClusterServices(cluster: Clus ...

Restricting a generic parameter to a combination type in Typescript

Is there a method in Typescript to restrict a generic parameter to only accept a union type? To clarify my question, I wish that T extends UnionType would serve this purpose: function doSomethingWithUnion<T extends UnionType>(val: T) {} doSomethingW ...

Replacing the '+' character with a space in HttpParams within Angular 6

When passing a JSON object using HttpParams, the + character is automatically converted to a space before being sent to the backend. Despite trying multiple solutions, I have been unable to resolve this issue for a JSONObject string. this.updateUser({"nam ...