CLI tool
Quramy/typed-css-modules avatar
Quramy/typed-css-modules

typed-css-modules: .d.ts files generated from CSS Modules

Creates .d.ts files from CSS Modules .css files

1,136 stars70 forksTypeScriptMIT

At a glance

What is it?
typed-css-modules reads your CSS Modules stylesheets and writes the matching TypeScript declaration files, so a class name in a stylesheet becomes a typed property instead of a string literal. It ships a tcm command with a watch mode and a check mode for CI, and its options follow the conventions of the webpack css-loader so that the generated types match what your bundler actually produces.
Who is it for?
Adopt typed-css-modules if you use CSS Modules with TypeScript and you want the compiler to catch a renamed or deleted class, and if you are willing to generate declaration files as a build step and commit them or check them in CI. Do not adopt it if your stylesheets are plain global CSS, since there are no module tokens to type, and do not adopt it if you would rather keep a hand written declaration file for a handful of classes.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A class name in a stylesheet is a string to the compiler

The problem this project solves is small and completely real. When you use CSS Modules, the class name you write in a stylesheet is not the same string as the one that ends up in the HTML, because the bundler rewrites it to a unique hash. TypeScript cannot see that relationship, so a component that imports a stylesheet and writes styles.myClass is, to the compiler, a property access on an untyped object, and a renamed class in the stylesheet is a silent break that shows up as a missing style at runtime.

The fix is to generate the declaration file. Given this stylesheet:

css
/* styles.css */

@value primary: red;

.myClass {
  color: primary;
}

the tool produces this:

ts
/* styles.css.d.ts */
declare const styles: {
  readonly primary: string;
  readonly myClass: string;
};
export = styles;

and the stylesheet can then be imported as a typed module:

ts
/* app.ts */
import styles from './styles.css';
console.log(`<div class="${styles.myClass}"></div>`);
console.log(`<div style="color: ${styles.primary}"></div>`);

Note what is in the declaration. The class names and the @value variables end up as readonly string properties on a single default export, and the file is named styles.css.d.ts, sitting next to the stylesheet it describes. The token names are the CSS identifiers, not the hashed output, because the hash is what the bundler produces at build time and it is the identifier your source code refers to.

The project is MIT licensed, the package is typed-css-modules, it requires Node 18 or later, and the current version is 0.9.1.

The tcm command and where it puts the files

The CLI is installed globally and pointed at a directory:

sh
npm install -g typed-css-modules
sh
tcm src

That creates the .css.d.ts files under the directory which has the original .css file, so the result looks like this:

text
(your project root)
- src/
    | myStyle.css
    | myStyle.css.d.ts [created]

The output location is adjustable with -o or --outDir, which is what you want when the generated files are build artefacts rather than source:

sh
tcm -o dist src

The search pattern is adjustable too. By default the tool searches for **/*.css files under the input directory, and --pattern or -p overrides it. The README flags a detail that saves a confusing afternoon: the quotes around the glob are required, so that your shell does not perform the expansion.

sh
tcm -p 'src/**/*.css' .

Two more flags complete the operational set. With -w or --watch the CLI watches files in the input directory, which is the mode you want while you are editing stylesheets. And with -l or --listDifferent the tool lists any files that are different than those that would be generated, and exits with a status code 1 if any are.

That last flag is the one that makes the tool usable in a pipeline. It turns the generated files into something that can be checked, so a pull request that renames a class without regenerating the declaration file fails rather than shipping.

The library is also usable as an API, and the README shows the shape of that:

js
import DtsCreator from 'typed-css-modules';
let creator = new DtsCreator();
creator.create('src/style.css').then(content => {
  console.log(content.tokens); // ['myClass']
  console.log(content.formatted); // 'export const myClass: string;'
  content.writeFile(); // writes this content to "src/style.css.d.ts"
});

The options mirror the CLI: rootDir for the project root, defaulting to process.cwd(), searchDir for the directory that includes the target .css files, defaulting to './', outDir for the output directory, defaulting to searchDir, plus camelCase, namedExports, allowArbitraryExtensions and EOL, which takes '\n' or '\r\n' and defaults to os.EOL.

The create method also takes an optional second argument, the CSS content, and uses it instead of the contents of the file on disk, which is what makes the library usable against a virtual file system rather than only against real files.

camelCase follows the css-loader rules rather than inventing its own

The option that causes the most confusion in tools like this is camelisation, because a kebab-cased class name is not a valid TypeScript identifier. typed-css-modules handles it with -c or --camelCase, and the subtlety is in the second form.

With --camelCase, kebab-cased CSS classes such as .my-class are exported as a camelised TypeScript variable name, export const myClass: string. With --camelCase dashes, only the dashes in the class name are camelised.

The README explains where that second mode comes from. The same behaviour has been available since version 0.27.1 in the webpack css-loader, and this option keeps upper case class names intact, so .SomeComponent stays .SomeComponent:

css
.SomeComponent {
  height: 10px;
}

becomes

typescript
declare const styles: {
  readonly SomeComponent: string;
};
export = styles;

The README links the webpack css-loader camelCase option, which is the right reference to read alongside it, because the two have to agree. The whole point of the tool is that the type it declares is the name your bundler will produce, and if the tool camelises differently from the loader, the types are correct and the styles are still missing.

That alignment is the design principle behind the option set, and it is worth checking for your own bundler before you adopt any of the flags. The default is no camelisation at all, which is correct for a build that does not rename classes, and the two modes exist for the builds that do.

namedExports for tree shaking, and the camelCase side effect

The -e or --namedExports option changes the shape of the generated file, and it comes with a behaviour you have to know about.

With named exports, types are exported as named exports as opposed to default exports. The stated reason is that this enables support for the namedExports css-loader feature, which is required for webpack to tree shake the final CSS. The README links both the css-loader namedExports option and the style-loader one, if you use style-loader.

The generated output changes accordingly, from a default export object to individual exports:

typescript
export const someComponent: string;

And the note is the part to notice. This option enables camelcase by default.

That is a convenience and a trap at once. It is convenient because the named export form is per identifier, so a kebab-cased class name has to be renamed to be a legal identifier. It is a trap because a project that had camelisation off and turns namedExports on silently gets camelised names, and the types still match the generated CSS if the loader is configured the same way, so nothing breaks loudly.

There is a related option for module resolution, -a or --allowArbitraryExtensions, which makes the output filenames compatible with the arbitrary file extensions feature introduced in TypeScript 5.0. In essence the *.css.d.ts extension becomes *.d.css.ts, so that you can import CSS modules in projects using ESM module resolution.

That last detail is the one to plan for. If your project uses Node style or bundler style ESM resolution, a declaration file named styles.css.d.ts is not what the compiler looks for, and the fix is the flag, not a tsconfig workaround.

Invalid tokens, messages and the writeFile hook

Two details of the API are worth knowing before you wire the tool into a build.

The first is what happens to a class name that cannot be a TypeScript identifier. The Remarks section says that if your input CSS file has certain class names, those invalid tokens are not written to the output .d.ts file, and the example is a TypeScript reserved word used as a class name, .while. The DtsContent object carries a messageList array for exactly this, described as an array of messages that contain invalid token information, with the example message 'my-class is not valid TypeScript variable name.'

So a token that cannot be represented is dropped from the declaration and reported, rather than producing a file that does not compile. That is the right behaviour, and it also means a silent gap is possible if nobody reads messageList, which is a reason to use the -l check mode in CI.

The second is the writeFile hook. writeFile takes an optional postprocessor, a function that takes the formatted definition string and returns a modified string that will be the final content written to the file. The README suggests using it to pass generated definitions through a formatter like Prettier, or to add a comment to the top of generated files:

js
dtsContent.writeFile(definition => `// Generated automatically, do not edit\n${definition}`);

That hook is the extension point that makes the generated files acceptable in a codebase with formatting rules or a convention of marking generated files, which is the difference between a tool people use and a tool people fight.

The DtsContent object also exposes contents as an array of TypeScript definition expressions, tokens as an array of the tokens found in the input, formatted as the string, and outputFilePath as the final output path.

Editorial conclusion

Adopt typed-css-modules if you use CSS Modules with TypeScript and you want the compiler to catch a renamed or deleted class, and if you are willing to generate declaration files as a build step and commit them or check them in CI. Do not adopt it if your stylesheets are plain global CSS, since there are no module tokens to type, and do not adopt it if you would rather keep a hand written declaration file for a handful of classes. Verify first that your bundler uses the same class name transformation as the tool, that the generated files match your module resolution mode, and that the check mode passes on a clean tree before you wire it into a pipeline.

Frequently asked questions

What does typed-css-modules generate?

It generates a .css.d.ts file next to each CSS Modules stylesheet, declaring the class names and @value variables as readonly string properties. For a stylesheet with .myClass and an @value primary, the declaration is a const styles object with readonly primary and readonly myClass, exported as a default export.

How do I install and run the tcm command?

Run npm install -g typed-css-modules and then tcm with your input directory, for example tcm src, which creates the .css.d.ts files next to the original .css files. Use -o or --outDir for a different output location and -p for a custom glob pattern, with the quotes around the pattern required.

How can I check that the generated type files are up to date?

Use the -l or --listDifferent option, which lists any files that differ from those that would be generated and exits with status code 1 if there are any. The -w or --watch option is for regenerating while you edit.

What does the namedExports option do and what should I watch out for?

With -e or --namedExports the types are emitted as named exports instead of a default export, which is required for the css-loader namedExports feature so that webpack can tree shake the final CSS. The README also notes that this option enables camelcase by default.

Official sources

  1. Issues
  2. License: MIT
  3. Quramy/typed-css-modules on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/quramy-typed-css-modules.svg)](https://hysenlabs.com/projects/quramy-typed-css-modules)

Was this useful?