CSS を読み込むだけの 1 行が、TypeScript を 6 に上げた tsc で止まります。
src/index.ts(1,8): error TS2882: Cannot find module or type declarations for side-effect import of './globals.css'.
エディタの日本語表示は次の形で報告されています(当プロジェクトで実測したのは上の英語出力で、こちらは日本語圏の報告からの引用です)。
'./globals.css' の副作用インポートに対するモジュールまたは型宣言が見つかりません。ts(2882)
TypeScript 6.0 で noUncheckedSideEffectImports の既定が false から true になりました。 5.9 までは、解決できない副作用 import(import "./globals.css" のように何も受け取らない import)は黙って無視されていました。6.0 からは型宣言が無いとエラーになります。
最小の修正は、tsconfig.json の include が拾う場所にアンビエント宣言を 1 つ置くことです。
// src/globals.d.ts
declare module "*.css";
ただしこの *.css は、多くの場合に足りません。 照合されるのは import に書いた文字列のほうで、解決先のファイル名ではないからです(後述)。
なぜ TypeScript 6 で出るのか:noUncheckedSideEffectImports の既定が反転した
同じソース・同じ tsconfig.json のまま、TypeScript の版だけを変えると境界が出ます。
// src/index.ts
import "./globals.css";
export const title = "errfix";
| TypeScript | 結果 | 終了コード |
|---|---|---|
| 5.9.3 | 通る | 0 |
| 6.0.3 | error TS2882 | 2 |
| 7.0.2 | 同じメッセージ | 1 |
7.0 も 6.0 の既定を継承します。メッセージは同一で、終了コードだけが 6.0 の 2 から 7.0 で 1 に変わります。
副作用 import は、名前を 1 つも受け取りません。だから型としては何も要らないように見えますが、TypeScript から見ると「そのモジュールが何なのか分からないまま実行だけさせている」状態です。noUncheckedSideEffectImports は、その状態を検査するかどうかの設定で、5.6 で追加されて 6.0 で既定が入れ替わりました。
このエラーは 2 つの別々の原因から出ます。 対象のファイルが存在しない場合も、存在するが型宣言が無い場合も、同じ文字列になります。
$ rm src/globals.css
$ npx tsc --noEmit
src/index.ts(1,8): error TS2882: Cannot find module or type declarations for side-effect import of './globals.css'.
メッセージの module or type declarations がそのまま両方を指しています。宣言を足す前に、まずパスが実在するかを確かめてください。
宣言を足したのに消えないとき:原因は 2 つある
declare module "*.css"; を書いたのにメッセージが 1 文字も変わらない、という形が 2 通りあります。症状が同じなので、両方を見てください。
原因 A:import に書いた文字列が .css で終わっていない
ワイルドカードが照合するのは import に書いた specifier です。解決先のファイル名ではありません。だから specifier がライブラリのサブパスで、その先が CSS に解決される形——import "somelib/styles" のように specifier 自体が .css で終わらない形——では、*.css は一致しません。
その形を最小の合成パッケージで作って測ると、こうなります(照合が specifier に対して行われることの再現であって、特定のライブラリの現況ではありません)。
// node_modules/fakelib/package.json
{ "name": "fakelib", "exports": { "./styles": "./styles.css" } }
import "fakelib/styles";
| 置いた宣言 | 結果 |
|---|---|
| なし | error TS2882 ... of 'fakelib/styles' |
declare module "*.css"; | 同じエラーのまま |
declare module "fakelib/styles"; | 通る |
declare module "fakelib/*"; | 通る |
fakelib/styles は .css で終わらないので、*.css のパターンに当たりません。宣言を足したのにメッセージが 1 文字も変わらないのはこの形です。specifier をそのまま書くか、declare module "fakelib/*"; のようにライブラリ側に合わせたパターンにしてください。
エラー文の of '...' に入っている文字列が、照合の対象です。 そこを見て宣言のパターンを決めてください。
なお、CSS を配るライブラリでこの形が報告されたことはありますが(vuetifyjs/vuetify#22766 など)、上流が型宣言を同梱すれば宣言は要らなくなります。実際、[email protected] の ./styles と @payloadcms/[email protected] の ./css はどちらも exports に types を持っており、この 2 つは現在そのままコンパイルが通ります。ライブラリ名で覚えるのではなく、エラー文に出ている specifier で判断してください。
原因 B:.d.ts がプログラムに入っていない
宣言ファイルは、tsconfig.json がプログラムに含めた場合にだけ読まれます。include: ["src"] だけを書いた構成では、src の外にあるファイルは入りません。同じ内容の declare module "*.css"; でも、置き場所だけで結果が変わります。
置き場所(include: ["src"] のとき) | 結果 |
|---|---|
src/globals.d.ts | 通る |
./globals.d.ts(ルート直下) | error TS2882 のまま |
include 以外にも、files に直接並べる・別のファイルから /// <reference path="..." /> で参照する・types に型パッケージとして載せる、といった経路でプログラムに入ります。「src の中に置く」が唯一の正解ではありません——確かめるのは置き場所ではなく、実際にプログラムへ入っているかです。npx tsc --showConfig の files に、その .d.ts が出ているかを見てください。
直し方:宣言を足すか、検査を止めるか
宣言を足す(既定の検査を保ったまま通す)。
// src/globals.d.ts — include の内側に置く
declare module "*.css";
declare module "fakelib/styles";
検査そのものを止める(5.9 までの挙動へ戻す)。
{
"compilerOptions": {
"noUncheckedSideEffectImports": false
}
}
どちらも同じソースで終了コード 0 になることを実測しています。差はこの先の取りこぼしです。false に戻すと、いま直したい CSS の import だけでなく、解決できない副作用 import すべてが 5.9 までと同じく黙殺されます。TypeScript はこのオプションを追加したとき、綴り誤りの副作用 import が見逃されることを動機として挙げています(当プロジェクトでは綴り誤りの側を実測していません)。移行を止めないための当座として使い、宣言を並べる形へ移す前提で入れてください。
Vite が用意する vite/client の型は *.css を宣言しているので、テンプレートが置く src/vite-env.d.ts の /// <reference types="vite/client" /> が残っている構成では、この宣言がすでにある状態になります(当プロジェクトでは Vite 構成を実測していません)。
切り分け
- 宣言を足したのにメッセージが変わらない:
.d.tsの置き場所(includeの内側か)と、パターンが specifier に一致しているか(.cssで終わらない specifier に*.cssを当てていないか)の 2 つを見てください。上の 2 節がその形です。 tscは通るのにエディタだけ赤い:エディタとコマンドラインが別の TypeScript か、別のtsconfig.jsonを使っています。まず両方の版と、エディタがどのtsconfig.jsonを選んでいるかを確かめてください(当プロジェクトではエディタ側を実測していません)。error TS2591: Cannot find name 'process'も一緒に出ている:同じ TypeScript 6.0 の既定変更の別件です。compilerOptions.typesの既定が[]になったほうで、原因も修正も別です。@types/nodeを入れてあるのにprocessが見つからない場合 を見てください。Option '...' has been removedが出ている:削除された compiler option の話で、こちらとは別の境界です。TypeScript が削除した compiler option を見てください。- CI の終了コードで分岐している:6.0 は 2、7.0 は 1 を返します。値ではなく成否で分岐してください。
検証環境
node@sha256:d649c27d...(node:22-bookworm-slim)、[email protected]、ネットワーク有り- 再現:
import "./globals.css"を持つsrc/index.tsでnpx tsc --noEmitがerror TS2882: Cannot find module or type declarations for side-effect importを出して終了コード 2 - 修正:
src/globals.d.ts(declare module "*.css";)というファイルを 1 つ足すだけで終了コード 0
再現から修正までは errfix の検証ハーネスが機械的に確認しています。版の対照(5.9.3 が終了コード 0、6.0.3 が 2、7.0.2 が 1)、.d.ts を include の外に置くと直らないこと、対象ファイルを削除しても同じメッセージになること、noUncheckedSideEffectImports: false が終了コード 0 に戻すこと、そしてサブパスが CSS に解決される specifier では declare module "*.css" が効かず、specifier に一致する宣言だけが通ることは、いずれも同じイメージで実測しています。
[email protected] の ./styles と @payloadcms/[email protected] の ./css が現在そのまま通ることも、同じイメージで実測しています(どちらも exports に types を持ちます)。
Vite・Next.js の実構成、エディタとコマンドラインで設定が食い違う場合、noUncheckedSideEffectImports: false にしたときの綴り誤りの扱いは実測していません。 エディタの日本語表示は日本語圏の報告からの引用です。