React TypeScript组件库SVG正确导出及编译异常排查
咱们一步步拆解你遇到的三个问题:
1. 为什么src/assets/icons/index.ts不被识别为模块?
这个报错的核心原因是TypeScript无法识别你从SVG文件导出的ReactComponent类型,进而认为整个icons/index.ts文件没有合法的导出,不构成一个模块。
看你的global.d.ts里的SVG模块声明:
declare module '*.svg' { const content: string; export default content; }
这里只声明了SVG默认导出是字符串(对应file-loader的处理方式),但你用babel-plugin-named-asset-import配合@svgr/webpack是把SVG导出为React组件,也就是每个SVG模块会有一个ReactComponent的具名导出——而你的类型声明里完全没提到这个,所以TypeScript在检查icons/index.ts里的export { ReactComponent as Add } from "./add.svg";时,会认为"./add.svg"没有ReactComponent这个导出,导致整个icons/index.ts的导出逻辑不合法,最终被判定为“不是一个模块”。
另外,你的webpack配置里同时存在ts-loader和babel-loader处理TS/TSX文件,且babel-loader的规则范围覆盖了ts-loader的规则,这会导致TypeScript类型检查和babel编译的顺序混乱——ts-loader在处理时会先做类型检查,这时候因为SVG类型声明缺失,直接抛出错误。
2. 两个内容一致的文件编译结果不同?
你提到根目录的index.ts和src/demo/index.ts内容完全一致,但后者报错,前者正常?这大概率是路径解析的差异导致的TypeScript检查范围不同:
- 根目录的
index.ts里的import { Add } from "../assets/icons";其实是一个无效路径(根目录往上走就出了项目目录),但因为你的webpack入口是这个文件,webpack在编译时可能会通过resolve规则补全路径,或者TypeScript在检查时,因为这个路径实际不存在,反而跳过了严格的类型检查;而src/demo/index.ts的../assets/icons是正确路径(指向src/assets/icons),所以TypeScript会严格检查后者的导入,触发了类型声明缺失的报错。
另外,tsconfig的include虽然包含了src/**/*,但根目录的index.ts是单独列出来的,TypeScript对根目录文件的检查规则和src目录下的文件可能存在细微差异,但核心还是路径的正确性导致的检查行为不同。
3. 你的配置错误与正确实现方案
配置错误总结:
- SVG的类型声明与实际导出不匹配(只声明了字符串默认导出,没声明
ReactComponent具名导出) - webpack中同时配置
ts-loader和babel-loader处理TS/TSX文件,导致处理顺序和类型检查冲突 - 根目录
index.ts的导入路径存在错误
正确实现方案:
第一步:修正SVG的类型声明
更新global.d.ts,添加ReactComponent的导出,适配@svgr/webpack的处理结果:
import React from 'react'; declare module '*.svg' { // 对应file-loader的字符串导出(如果需要保留的话) const content: string; // 对应@svgr/webpack的React组件导出 const ReactComponent: React.FC<React.SVGProps<SVGSVGElement>>; export { ReactComponent }; export default content; }
第二步:优化webpack配置,避免loader冲突
推荐用babel-loader配合@babel/preset-typescript来处理TS/TSX,而不是同时用ts-loader和babel-loader——这样既能处理类型检查,又能处理编译,还能更好地配合babel-plugin-named-asset-import。
修改webpack.config.js:
const path = require("path"); module.exports = { entry: "./index.ts", module: { rules: [ { test: /\.(js|mjs|jsx|ts|tsx)$/, exclude: /node_modules/, use: { loader: require.resolve("babel-loader"), options: { customize: require.resolve("babel-preset-react-app/webpack-overrides"), presets: [ // 添加TypeScript预设 require.resolve("@babel/preset-typescript") ], plugins: [ [ require.resolve("babel-plugin-named-asset-import"), { loaderMap: { svg: { ReactComponent: "@svgr/webpack?-svgo,+ref![path]", }, }, }, ], ], }, }, }, // 如果你还需要处理非TS/JS的SVG(比如直接导入作为字符串),可以单独加一个规则 { test: /\.svg$/, exclude: /src\/assets\/icons/, // 排除被ReactComponent导入的图标 use: ["file-loader"], }, ], }, resolve: { extensions: [".tsx", ".ts", ".js"], }, output: { path: path.resolve(__dirname, "../dist"), filename: "index.js", libraryTarget: "commonjs2", // 适配组件库的导出格式 }, };
第三步:修正导入路径
确保根目录的index.ts导入路径正确:
import { Add } from "./src/assets/icons"; export default { Add };
第四步:调整tsconfig.json(可选但推荐)
因为用babel处理TypeScript,类型检查可以交给tsc单独做,所以可以把tsconfig.json里的noEmit设为true,然后单独用tsc --emitDeclarationOnly生成类型声明文件:
{ "include": ["./global.d.ts", "./index.ts", "src/**/*"], "compilerOptions": { "allowJs": false, "jsx": "react", "esModuleInterop": true, "downlevelIteration": true, "declaration": true, "noEmit": true, // 让babel负责编译,tsc只做类型检查和生成声明 "outDir": "dist", "moduleResolution": "node", "module": "commonjs", "target": "es5", "lib": ["es5", "es6", "es7", "es2017", "dom"], "sourceMap": true, "typeRoots": ["./node_modules/@types"], "resolveJsonModule": true } }
然后在package.json里添加脚本:
{ "scripts": { "build": "webpack --config ./config/webpack.config.js && tsc --emitDeclarationOnly" } }
这样执行npm run build就能同时完成编译和类型声明生成。
内容的提问来源于stack exchange,提问作者TheKvist

