A little trick for CSS/SCSS module safety
Tags: webdev, javascript, css, testing
Published: 2024-04-05 22:05:24

This is a tip for those using CSS Modules. You know, things like: 

`import styles from './componentStyles.module.scss'`

While we may never have anything near the "type safety" of TypeScript in styling with SCSS or CSS, we can take advantage of a quirk in how CSS Modules work to make it so that we will be warned if we try to access a style in our stylesheet that is undefined. 

This actually comes up quite a bit - you may have a defined style (in BEM syntax) of something like `.home__button__main {` but reference it in your React code as 
`<button className={styles.home__main__button}>Text</button>`. Or any number of typos.  The point is, if you try to access a value on the `styles` object that is undefined, it will return `undefined` which is a valid value, and which will be interpreted by your browser as `"undefined"` leading to elements having class `.undefined`. 

Wouldn't it be great if we could get our browser to throw an error at build time or run time if we attempted to use a property on `styles` that just wasn't there? 

We can. 

This is because style modules are interpreted in the JavaScript code as objects.  In Typescript, they'd be considered `type StyleModule = Record<string, string>`

Here's the cool bit. In JS, there's a rarely used set of keywords: "get" and "set".  Getters and setters often make code more complicated and hard to follow - that's why they're used sparingly, and many people prefer the syntax of creating a getter function. Setters are even more confusing, because they can execute arbitrary code logic whenever assigning a variable.  There are a few cases in which this might be useful. For example: 

```typescript
class Weight {
  constructor(private value: number){}

  get kilograms (){
    return this.value;
  }
  get pounds (){
    return this.value * 2.2;
  }
  set kilograms (value: number){
    this.value = value;
  }
  set pounds = (value: number) {
    this.value = value / 2.2
  }
}

const weight = new Weight (10);
console.log(weight.kilograms); // 10
console.log(weight.pounds); // 22.0
weight.kilograms = 5;
console.log(weight.pounds); // 11
weight.pounds = 44
console.log(weight.kilograms); // 20
```
And so on and so forth. 

Using this, we can actually run code *on setting or retrieving values*, and change the output conditionally.  So what does this mean? 

It means we can write something like this: 

```typescript
export const makeSafeStyles = (
    style: Record<string, string>,
    level: "strict" | "warn" | "passthrough" = "strict"
): Record<string, string> => {
    const handler = {
        get(target: Record<string, string>, prop: string, receiver: any): any {
            // if element is defined, return it. 
            if (prop in target) {
                return Reflect.get(target, prop, receiver);
            }
            // otherwise, 
            if (level === "strict") {
                throw new TypeError(`This class has no definition: ${prop}`);
            }
            if (level === "warn") {
                console.warn(
                    `This class has no definition: ${prop}. Defaulting to 'undefined ${prop}`
                );
            }
            // we should 
            return `undefined_class_definition ${prop}`;
        },
    };
    return new Proxy(style, handler);
};
```

The result: When you use an undefined style in a project... this happens. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2de9zbnd3tfkz2iuj0fq.png)

I can go into the code and see: 

```tsx
 <div className={styles["progress-bar-outer"]}>
                <div
                    className={styles["progress-inner-bar"]}
                    style={{ width: `${formCompletionPercentage * 100}%` }}
                ></div>
            </div>
```
and to my module and see
```scss
.progress-bar-outer {
    position: relative;
    width: 100%;
    height: $progress-bar-height;
    background-color: get-palette("progress-bar-background");
    margin: 0;
}
.progress-bar-inner {
    position: absolute;
    width: 0%;
    height: $progress-bar-height;
    transition: width ease-out 0.25s;
    background-color: get-palette("progress-bar-fill");
}
```
And I immediately know that there is a typo or spelling error that is simply fixed by correcting the line: 

```tsx
 <div className={styles["progress-bar-outer"]}>
                <div
                    className={styles["progress-bar-inner"]}
                    style={{ width: `${formCompletionPercentage * 100}%` }}
                ></div>
            </div>
```