1

I need to make a recursive style object, that can support style properties and in a nested format.

I have a super hard time wrapping my head around this, and I've tried pretty much all solutions I could find here on SO and google.

interface Properties {
  border?: string;
  width?: string;
}

//# 1 Type attempt
type TRecursiveProperties = Properties & {
  [index: string]: TRecursiveProperties;
};

//# 2 Interface attempt
interface IRecursiveProperties extends Properties {
  [index: string]: IRecursiveProperties;
}


const test: TRecursiveProperties = {
  border: '1px solid green',
  isActive: {
    border: '2px solid red',
    '&:hover': {
      border: '3px solid blue'
    }
  }
};

I'd expect the Recursive properties to be a fallback/catch all or some way to exclude keys from Properties object.

The 2 errors I get are either

Type alias 'TRecursiveProperties' circularly references itself.

Property 'width' of type 'string' is not assignable to string index type 'IRecursiveProperties'

Any ideas how I can achieve that?

1

2 Answers 2

2

There is no concrete type that accurately represents what you're trying to do: namely to "special-case" some properties and exclude them from the index signature. It has been requested, but so far there's no way to do it.

Notice that I said there's no concrete type. You can represent it as a generic type to which you constrain a value. So instead of having a value of type RecursiveProperties, you have one of type T extends VerifyRecursiveProperties<T>. Like this:

type VerifyRecursiveProperties<T> = Properties & { [K in Exclude<keyof T, keyof Properties>]:
  T[K] extends object ? VerifyRecursiveProperties<T[K]> : never }

And then you need a helper function to infer the particular T without having to write it out yourself:

const asRecursiveProperties = <T extends VerifyRecursiveProperties<T>>(t: T) => t;

This lets you do what you wanted:

const test = asRecursiveProperties({
  border: '1px solid green',
  isActive: {
    border: '2px solid red',
    '&:hover': {
      border: '3px solid blue'
    }
  }
}); // okay

and also gives errors if you violate the constraint:

asRecursiveProperties({
  border: 1 // error, number is not string
})

asRecursiveProperties({
  isActive: "" // error, string is not never
})

asRecursiveProperties({
  foo: {
    bar: {
      baz: {
        border: 1, // error, number is not string
      }
    }
  }
})

If that's too complicated, you might want to either loosen the constraint to allow the index signature to accept string | undefined (as in the other answer), or refactor your type so that you don't try to shove your Properties properties into the same object as your recursive properties, like this:

interface RefactoredRecursiveProperties extends Properties {
  nested?: { [k: string]: RefactoredRecursiveProperties }
}

const test2: RefactoredRecursiveProperties = {
  border: '1px solid green',
  nested: {
    isActive: {
      border: '2px solid red',
      nested: {
        '&:hover': {
          border: '3px solid blue'
        }
      }
    }
  }
}

This refactoring might not be ideal for you, but it's much more straightforward for the compiler to reason about.


Okay, hope that helps; good luck!

Link to code

Sign up to request clarification or add additional context in comments.

2 Comments

Thank you for taking the time to provide so much details. First solution is perfect, but I'm missing the autocompletion of the properties (border doesn't show up as I'm typing :/) Anything I can do to get that to show up again?
I ended up going with the simple version from previous answer, but have upvoted this as well. Thanks again for the help!
1

I would go for this:

interface Properties {
  width?: string;
  border?: string;
  [selector: string]: string | Properties | undefined;
}

const myStyles: Properties = {
  'width': '100px',
  ' .child': {
    'width': '200px',
    'border': '1px color blue',
    '&:hover': {
      border: '1px solid aquamarine',
    },
  },
};

In this typescript resource: https://basarat.gitbook.io/typescript/type-system/index-signatures, search for 'Design Pattern: Nested index signature' to see a pretty similar example.

1 Comment

Thanks for the answer, I love the simplicity of this, I ended up going with this. 🙌

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.