What is the best way to define a recursive object type in TypeScript specifically for managing CSS styles?

I have implemented React.CSSProperties to generate css variables, allowing me to define styles like

let css: React.CSSProperties = {
  position: "relative",
  display: "inline-block",
  background: "red"
}

My goal is to build a theme with css variables at the root level or nested within modifiers such as size (sm, md, lg) or variant (light, dark) or others, as shown below:

let theme: MyTheme = {
  background: "red",       // css
  color: "white",          // css  

  button: {                // custom: component name Level1
    background: "red",     // css
    color: "white",        // css

    light: {               // custom: modifier variant Level2
      background: "red",   // css
      color: "white",      // css

      hollow: {            // custom: modifier Level3
        borderColor: "red",
        color: "red",
        background: "transparent",
      }
    }
    dark: {
      background: "red",
      color: "white",

      hollow: {
        borderColor: "black",
        color: "black",
        background: "transparent",

        modfierLevel4: {
          some: "css"

          modfierLevel5: {
            some: "css"

            modfierLevel6: {
              some: "css"
            }
          }
        }
      }
    }
  }
}

Essentially, I am looking for a recursive object type in typescript similar to the example below, but encountering circular reference issues:

type Modifiers = 'button' | 'dark' | 'light' | 'otherModifiers'

interface Theme extends React.CSSProperties {
 [k in Modifiers]?: Theme;
}

I found a solution that is close to what I need:

type Theme = React.CSSProperties & { [keys in Modifiers]?: Theme }

However, I am facing an issue. If an invalid modifier name like "btn" instead of "button" is provided:

  • An error is correctly thrown at level 1 (as expected)
  • No error is thrown from level 2 and above (even though it should be)

How can I define types to handle this scenario?

Answer №1

If you are working with TS3.4 or an older version:

There is a known issue detailed in this GitHub thread, where nested intersection types like Theme do not undergo excess property checks as expected. In TypeScript, types are usually considered "open" or "extendable", allowing values to have additional properties beyond what's defined in the type. For instance, defining interface A { a: string } and extending it with

interface B extends A { b: string }
permits values of type B to also be treated as values of type A.

However, when working with object literals, adding extra properties can often lead to errors. Particularly in cases where the "extra" property is simply a typo for an optional property. The compiler aims to enforce strictness by flagging extra properties when assigning new object literals to a type. Freshly created object literals are meant to be "closed" or "exact", but there is a bug that impacts nested intersections in TypeScript versions 3.4 and below.

In TypeScript 3.4 or before, this behavior can be observed:

type Foo = { baseProp: string, recursiveProp?: Foo };
const foo: Foo = {
    baseProp: "a",
    recursiveProp: {
        baseProp: "b",
        extraProp: "error as expected" // error 👍
    }
}

type Bar = { baseProp: string } & { recursiveProp?: Bar };
const bar: Bar = {
    baseProp: "a",
    recursiveProp: {
        baseProp: "b",
        extraProp: "no error?!" // no error in TS3.4 😕
    }
}

You may explore the Playground link for further examples.

Fortunately, this issue has been addressed and resolved starting from TypeScript 3.5, with an anticipated release on May 30, 2019. If you cannot wait until then or upgrade immediately, you can implement a workaround by using a mapped type to replace the problematic intersection with a standard-looking object:

type Id<T> = { [K in keyof T]: T[K] };
type Bar = Id<{ baseProp: string } & { recursiveProp?: Bar }>;
const bar: Bar = {
    baseProp: "a",
    recursiveProp: {
        baseProp: "b",
        recursiveProp: {
            baseProp: "c",
            extraProp: "error yay" 👍
        }
    }
}

Refer to the Playground link for clarity on resolving the issue. Best of luck!

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

Breaking and Conquering Strategy: Locating the Smallest Value within a Matrix

I've been diving into divide and conquer algorithms, but I'm hitting a wall with this particular exercise. I've managed to create an algorithm that can find the minimum value in a vector using the divide and conquer approach: int minimum (i ...

The compiler detected an unanticipated keyword or identifier at line 1434. The function implementation is either absent or not placed directly after the declaration at line 239

Having trouble implementing keyboard functionality into my snake game project using typescript. The code is throwing errors on lines 42 and 43, specifically: When hovering over the "window" keyword, the error reads: Parsing error: ';' expecte ...

Troubleshooting ng-bind-html issue with displaying images loaded using CSS within HTML content

I followed the instructions provided in a related post on the same topic. I used 'ngSanitize' and formatted the HTML content using 'sce.trustAsHtml()', and it rendered correctly when the HTML contained both text and images. However, I e ...

I am perplexed by JQuery. It is returning true when comparing an input value from a number type input, and I cannot figure out the reason behind it

Seeking guidance on a perplexing issue. Here is a basic code snippet to depict my dilemma: html: <form> <input type="number" id="someNumber"> <input type="button" id="submitBtn"> </form> jquery: $("#submitBtn"). ...

Create a new chart in React using Chart.js when the button is clicked

I am currently working on a bar chart that dynamically adjusts its data based on the selected year. The name of the year is passed from the main component to the chart component in the following manner: Main Component: <DonateChart year={this.state.ac ...

Finding the current location's latitude and longitude in React Native for iOS

I am struggling to retrieve the longitude and latitude as I cannot seem to access it from the default geolocation JSON object that is returned. I have followed an example from the documentation (found here) and it worked well. However, I only require the ...

Animating the size of an element with dynamic content

I am working on a feature where clicking on a card should display a summary. When a new summary is shown, I want the summary container to animate and align its position with the associated card (top). However, despite my efforts, I am facing an issue whe ...

Typescript UniqueForProp tool not working as expected

I've created a utility function called UniqueForProp that takes an array of objects and a specified property within the object. It then generates a union type containing all possible values for that property across the array: type Get<T, K> = K ...

Is it possible to showcase dates within input text boxes prior to POSTing the form?

I am currently working on a form that posts to a PHP script by selecting a date range. For a better understanding, you can check out my visual representation here. Before the data is posted, I would like to display the selected dates in empty boxes or inpu ...

Determining the Presence of a Value in an Array using .includes

I've been facing challenges trying to use .includes with my array. My goal is to have a function that checks if a value already exists and removes it from the array if it does. The value is a string obtained from an object with a .name property. 0: { ...

Arranging a group of objects in Angular2

I am facing challenges with organizing an array of objects in Angular2. The structure of the object is as follows: [ { "name": "t10", "ts": 1476778297100, "value": "32.339264", "xid": "DP_049908" }, { "name": "t17", "ts": 14 ...

Tips for setting the draggable attribute on an <img> within the Next.js Image component

Is there a way to prevent dragging of ghost images? The usual method is using: <img draggable="false"> But how can we achieve the same with <Image /> component? Refer to the Image component documentation here: https://nextjs.org/d ...

Utilize the RGB function with React's Material Box feature

I'm having some trouble using the rgb function in Material Box. How can I achieve this successfully? The current method I am using is not working as expected. Original Code: <Box width={24} style={{ background: `rgb(${appoi ...

Is there a way to adjust the opacity of a background image within a div?

I have a lineup of elements which I showcase in a grid format, here's how it appears: https://i.stack.imgur.com/JLCei.png Each element represents a part. The small pictures are essentially background-images that I dynamically set within my html. Up ...

Making sure all items in the top row of Flexbox columns have the same height

Within my flex wrapper, I have multiple column components with various flex items inside. Each column displays content adjacent to each other. The challenge is to ensure that the top item in each column has an equal height, creating a row-like effect. I&a ...

An error occurred with Next.js and Lottie where the property "completed" could not be added because the object

Encountering an issue with Lottie's animation. Attempting to retrieve a JSON file (Lottie Animation) from Contentful and display it using the Lottie Component. However, facing an error message: "TypeError: Cannot add property completed, the object is ...

Enhancing search capabilities in Angular 8.1.2 by filtering nested objects

I am in need of a filter logic similar to the one used in the example posted on this older post, but tailored for a more recent version of Angular. The filtering example provided in the older post seems to fit my requirements perfectly, especially with ne ...

Ways to adjust the CSS height of a fluid body and flexible footer

In a nutshell, I'm dealing with 3 elements: <div> <Header/> </div> <div> <Body/> </div> <div> <Footer/> </div> The header has a fixed height. ...

Struggles with Aligning CSS Layout

I'm encountering some challenges with the layout of my webpage, particularly with alignment. Here are the issues I'm facing: The login section doesn't fit well in the header and lacks proper alignment. I aim to replicate the design from ...

What does this.userSubject = new BehaviorSubject<User>(JSON.parse(localStorage.getItem('user'))); signify?

I'm attempting to deconstruct this specific code snippet to comprehend it better. The new BehaviourSubject appears to be a function call, but I'm confused about what it's doing there. Is it instructing the function BehaviourSubject to have a ...