In a TypeScript project, `import styles from './Button.module.css'` fails with "Cannot find module './Button.module.css' or its corresponding type declarations." How do you make the compiler accept that import, and does your fix change anything at runtime?
answer
- types for a non-code import
- wildcard ambient module declaration
- the .d.ts must stay import-free
- tsconfig include has to reach it
- checker satisfied, bundler still required
basics
~20 sAdd a wildcard ambient module declaration in a .d.ts file the project includes: declare module '*.module.css' with a default export of a string-to-string map. That only satisfies the type checker — a bundler or loader must still make the import work at runtime.
solid answer
~40 sTypeScript knows nothing about CSS, so the specifier resolves to no type information. You fix it with a **wildcard ambient module declaration** in a `.d.ts` file, for example `declare module '*.module.css' { const classes: { readonly [key: string]: string }; export default classes; }`. Two conditions matter: that file must contain no top-level `import`/`export`, otherwise it becomes a module and the same line is read as an *augmentation* of a module that does not exist; and the file must actually be part of the program, i.e. matched by `include` or `files` in tsconfig, since nothing imports a `.d.ts`. Runtime behaviour is unchanged — the declaration emits no JavaScript. A bundler or loader still has to turn the CSS into a module, and the same import in plain Node would throw.
code
typescript · 10 lines// src/types/assets.d.ts — no top-level import or export in this file
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.svg' {
const url: string;
export default url;
}go deeper
Know the fix by heart: a .d.ts containing declare module '*.module.css' with a default export, placed where tsconfig picks it up. Say plainly that it satisfies the compiler only.
Explain why the file must have no top-level import or export, and how the same syntax flips from declaring an ambient module to augmenting one. Mention that .d.ts files enter the program only through include or files.
Show judgment about the declared shape: a loose string index signature hides typos, so reach for noUncheckedIndexedAccess or generated per-file declarations. Be able to diagnose the editor-vs-CI split caused by a declaration file outside the build's include globs.
Own the asset-typing convention: one declarations file or generated types, who maintains it, and how far the team is willing to let hand-written promises about the bundler drift from what the bundler actually does.
## What the error is actually saying The message "Cannot find module ... or its corresponding type declarations" is a *type* error, not a file-not-found error. The compiler resolved the specifier as a module and found no type information describing what that module exports. TypeScript ships type knowledge for JavaScript and for declaration files; it has no built-in notion of CSS, SVG, or any other non-code asset. Turning `./Button.module.css` into something importable is a build-tool job, and describing its shape to the checker is your job. ## Wildcard ambient module declarations An *ambient* declaration describes something that exists without defining it. An ambient **module** declaration names a module specifier and describes its exports: ```ts // src/types/assets.d.ts declare module '*.module.css' { const classes: { readonly [key: string]: string }; export default classes; } declare module '*.svg' { const url: string; export default url; } ``` The name may contain a single `*` wildcard, and any import specifier matching the pattern picks up those types. When two patterns both match a specifier, the more specific pattern is the one used, so you can declare `'*.module.css'` separately from a broader `'*.css'`. ## Condition one: the file must be a script, not a module TypeScript classifies every file by a single rule: a file with a top-level `import` or `export` is a **module**; a file with neither is a **script**, and its declarations land in the global scope. Inside a script, `declare module 'x' { ... }` *declares* an ambient module. Inside a module, the identical syntax means something different — it *augments* an existing module — and augmenting a module that does not resolve is an error. So adding one innocent `import type { Foo } from './foo'` at the top of your `assets.d.ts` breaks every wildcard declaration in it. If you need an imported type, use an inline import type instead, which does not make the file a module: ```ts declare module '*.module.css' { const classes: import('./css-types').ClassMap; export default classes; } ``` ## Condition two: the file must be in the program No source file ever imports a `.d.ts`, so the only way it enters compilation is by being matched by `files` or `include` in `tsconfig.json`. A very common failure is putting the declarations in a `types/` directory that the include globs do not cover: the editor may pick it up through a different config while `tsc` in CI does not, producing the classic "works locally, fails in the build" report. When a declaration seems ignored, first confirm the file is in the program at all. ## Nothing changes at runtime This is the point interviewers are usually probing. `declare module` emits no JavaScript whatsoever — like every part of the type layer, it is erased. The emitted code still contains an import of a `.css` file, and something has to make that import mean something: a bundler with a CSS-modules loader, or a runtime that understands the extension. If you compile with `tsc` and run the output in Node, the import throws, and the declaration will not have warned you. The declaration is a *promise* about a shape you arranged elsewhere; the compiler trusts it without verification. ## Shaping the declaration honestly `{ [key: string]: string }` is convenient but permissive: `styles.buton` type-checks and is `string`, so a typo survives to runtime as `undefined`. Two ways to tighten it: enable `noUncheckedIndexedAccess`, which makes every index access `string | undefined` and forces you to handle the miss; or generate a precise per-file declaration (`Button.module.css.d.ts` listing the real class names) with a typed-CSS-modules tool, at the cost of a generation step in the build. ## Related cases Image and font imports follow the same pattern with `const url: string`. JSON is the exception: instead of declaring `'*.json'` yourself, turn on the `resolveJsonModule` compiler option, which makes the compiler read the JSON file and infer its exact type — a far better result than a hand-written wildcard.
- Why does adding a single top-level import to that .d.ts file break the wildcard declaration?Because a top-level import turns the file from a script into a module. In a script, `declare module '*.module.css'` declares a new ambient module; in a module, the same syntax means augmenting an existing module, and there is no real module named `*.module.css` to augment, so the compiler errors. Use an inline `import('./x').Type` if you need an external type.
- Your declaration types class names as a plain string index signature. What does that cost you, and how would you tighten it?Every property access type-checks, so `styles.buton` is `string` and the typo reaches runtime as `undefined`. Enable `noUncheckedIndexedAccess` so index access yields `string | undefined` and misses must be handled, or generate an exact per-file declaration listing the real class names with a typed-CSS-modules tool, accepting the extra build step.
- Would you use the same wildcard approach for importing a JSON file?No. TypeScript has a dedicated compiler option, `resolveJsonModule`, which makes the compiler read the JSON file and infer its precise type — literal keys and value types included. A hand-written `declare module '*.json'` would throw all of that away and give you one imprecise shape for every JSON file in the project.
saying these in an interview costs you the question
- Thinks the declaration makes the CSS import work at runtime
- Says TypeScript needs a plugin to understand CSS files
- Puts the wildcard in a file that also imports something
- Writes the .d.ts outside the tsconfig include globs
- Believes a string index signature catches class-name typos