如何使用 TypeScript 重写并生成声明文件
解读
面试官抛出这道题,并不是让你把 Grunt 源码全量用 TS 重写一遍,而是考察三件事:
- 你是否理解 Grunt 插件的“任务函数签名”与“多目标配置范式”;
- 能否把既有的 JavaScript 任务插件迁移到 TypeScript,并一次性产出 .d.ts 声明文件,让后续团队在其他 TS 项目里获得类型提示;
- 是否熟悉国内前端工程化落地时的“渐进式迁移”策略——老项目还在跑 Grunt,新模块先用 TS 写,声明文件必须随包发布到私有 npm,否则协作会翻车。
因此,回答时要给出“最小可运行骨架 + 声明文件自动生成方案”,并强调如何兼容 Grunt@1.x 的 CommonJS 加载规则,否则会被追问“为什么不用 ESM”。
知识点
- Grunt 任务插件的固定导出格式:
module.exports = function(grunt: IGrunt): void; - @types/grunt 与 @types/node 提供的全局类型,避免自己手写
IGrunt接口。 - tsconfig 的 declaration: true + outDir 是官方推荐的声明文件生成方式,但必须配套 commonjs 模块格式,否则 Grunt 运行时加载会报
grunt.registerTask is not a function。 - npm 包发布时的 files 字段要显式包含
lib/**/*.d.ts,否则私有 npm 仓库会丢声明文件,这是国内踩坑高频点。 - grunt-contrib- 插件自身仍是 JS*,你的 TS 插件需要向下兼容其配置结构,例如
src | dest | expand | ext等,类型定义里要用 Partial<> 保持可选,避免破坏老项目配置。
答案
-
初始化
mkdir grunt-contrib-xxx-ts && cd $_ npm init -y npm i -D typescript @types/grunt @types/node grunt -
配置 tsconfig.json(关键:commonjs + declaration)
{ "compilerOptions": { "target": "ES2018", "module": "commonjs", "lib": ["ES2018"], "outDir": "lib", "declaration": true, "strict": true, "esModuleInterop": true }, "include": ["src"] } -
编写 src/tasks/xxx.ts(严格遵循 Grunt 插件签名)
import * as grunt from 'grunt'; interface IXxxOptions { banner?: string; stripConsole?: boolean; } function xxxTask(this: grunt.task.IMultiTask<IXxxOptions>): void { const options = this.options<IXxxOptions>({ banner: '', stripConsole: false }); this.files.forEach(f => { const combined = f.src.map(p => grunt.file.read(p)).join('\n'); const bannered = options.banner + combined; grunt.file.write(f.dest, bannered); grunt.log.ok(`File ${f.dest} created.`); }); } export = (grunt: IGrunt): void => { grunt.registerMultiTask('xxx', 'TypeScript rewrite of xxx', xxxTask); }; -
编译 & 生成声明
npx tsc产物结构:
lib/ tasks/ xxx.js xxx.d.ts -
发布配置(国内私有 npm 必看)
package.json 追加:"main": "lib/tasks/xxx.js", "types": "lib/tasks/xxx.d.ts", "files": ["lib"]执行
npm publish即可把.d.ts一起推到私有仓库,否则同事安装后仍提示“找不到类型”。 -
老项目无痛接入
npm i grunt-contrib-xxx-ts@1.0.0 -DGruntfile.js 完全零改动,因为任务名仍是
xxx,配置结构也未变,只是背后逻辑已用 TS 重写并自带类型提示。
拓展思考
- Monorepo 场景:如果公司用 pnpm workspace,把 TS 版 Grunt 插件作为内部包,记得在根目录
pnpm-workspace.yaml里声明"packages/*",否则类型解析路径会飘。 - 性能优化:Grunt 任务默认同步执行,TS 编译后仍是同步,若任务重 IO,可内部改用
grunt.util.async或Promise,但注册方式仍要保持回调风格,否则 Grunt 会提前结束。 - 未来迁移:一旦团队全面切到 Vite/Rollup,可把同一套 TS 源码改写成 Rollup 插件,复用类型定义,实现“同一内核,双构建引擎”,降低二次开发成本,这也是国内大厂渐进式演进的常用套路。