reactjstypescriptmaterial-ui

Can you define a type that has fixed keys at object creation, with values of a given type?


I'm using MaterialUI's sx property to style some react components. I find having the full style definition inline with the components leads to messy, screen filling component bodies, so I have moved all the style definitions into a constant object outside the function component. Here's a quick example:

const styles = {
    a: {/*...*/}
    b: {/*...*/}
}

const component = () => <Box sx={styles.a}>
        <Button sx={styles.b}>Button Text</Button>
    </Box>

The problem I'm having is ensuring that styles.a and styles.b are valid objects, which is difficult without the IDE knowing what types they should be. If I declare styles: Record<string, SystemStyleObject> I gain type checking when defining a and b, but I lose knowledge over the actual keys of styles and accessing styles.c no longer errors.

I've come up with two clunky work-arounds:

#1 Declare the individual variables and then collect them into an object

const a: SystemStyleObject = {}
const b: SystemStyleObject = {}

const styles = {a, b}

This gets me to the end product I want, but it requires declaring every single style in a separate variable, as well as explicity typing them.

#2 Pass a union of the objects keys as the first argument to record

const styles: Record<'a' | 'b', SystemStyleObject> = {
    a: {/*...*/}
    b: {/*...*/}
}

This also gets me to the same end product, but makes adding or changing the keys of the object awkward as you have to add them to the union as well as the body.

#2 is the one I've gone with for now, but I'm wondering if there's a better way to achieve this.


Solution

  • This is now possible in ts 4.9 with the satisfies keyword. This validates the type but does not corece the result into the wider type.

    interface thing {
      prop: string;
    }
    
    const stuff = {
      this: {
        prop: "hello",
      },
      that: {
        // Errors as prop is not defined
      },
      something: {
        prop: "test",
        otherProp: 4,
        // Errors as otherProp does not exist in the interface.
      }
    } satisfies Record<string, thing>
    
    console.log(stuff.this.prop);
    
    console.log(stuff.what.prop);
    // Errors as stuff.what is not defined
    

    The final type of stuff is not Record<string, thing> it is

    {
       this: {
          prop: string;
       }
       that: {}
       something: {
          prop: string;
          otherProp: number;
       }
    }