如何使用 TypeScript 重写并生成声明文件

解读

面试官抛出这道题,并不是让你把 Grunt 源码全量用 TS 重写一遍,而是考察三件事:

  1. 你是否理解 Grunt 插件的“任务函数签名”与“多目标配置范式”;
  2. 能否把既有的 JavaScript 任务插件迁移到 TypeScript,并一次性产出 .d.ts 声明文件,让后续团队在其他 TS 项目里获得类型提示;
  3. 是否熟悉国内前端工程化落地时的“渐进式迁移”策略——老项目还在跑 Grunt,新模块先用 TS 写,声明文件必须随包发布到私有 npm,否则协作会翻车。

因此,回答时要给出“最小可运行骨架 + 声明文件自动生成方案”,并强调如何兼容 Grunt@1.x 的 CommonJS 加载规则,否则会被追问“为什么不用 ESM”。

知识点

  1. Grunt 任务插件的固定导出格式module.exports = function(grunt: IGrunt): void;
  2. @types/grunt@types/node 提供的全局类型,避免自己手写 IGrunt 接口。
  3. tsconfig 的 declaration: true + outDir 是官方推荐的声明文件生成方式,但必须配套 commonjs 模块格式,否则 Grunt 运行时加载会报 grunt.registerTask is not a function
  4. npm 包发布时的 files 字段要显式包含 lib/**/*.d.ts,否则私有 npm 仓库会丢声明文件,这是国内踩坑高频点
  5. grunt-contrib- 插件自身仍是 JS*,你的 TS 插件需要向下兼容其配置结构,例如 src | dest | expand | ext 等,类型定义里要用 Partial<> 保持可选,避免破坏老项目配置。

答案

  1. 初始化

    mkdir grunt-contrib-xxx-ts && cd $_
    npm init -y
    npm i -D typescript @types/grunt @types/node grunt
    
  2. 配置 tsconfig.json(关键:commonjs + declaration

    {
      "compilerOptions": {
        "target": "ES2018",
        "module": "commonjs",
        "lib": ["ES2018"],
        "outDir": "lib",
        "declaration": true,
        "strict": true,
        "esModuleInterop": true
      },
      "include": ["src"]
    }
    
  3. 编写 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);
    };
    
  4. 编译 & 生成声明

    npx tsc
    

    产物结构:

    lib/
      tasks/
        xxx.js
        xxx.d.ts
    
  5. 发布配置(国内私有 npm 必看
    package.json 追加:

    "main": "lib/tasks/xxx.js",
    "types": "lib/tasks/xxx.d.ts",
    "files": ["lib"]
    

    执行 npm publish 即可把 .d.ts 一起推到私有仓库,否则同事安装后仍提示“找不到类型”

  6. 老项目无痛接入

    npm i grunt-contrib-xxx-ts@1.0.0 -D
    

    Gruntfile.js 完全零改动,因为任务名仍是 xxx配置结构也未变,只是背后逻辑已用 TS 重写并自带类型提示。

拓展思考

  1. Monorepo 场景:如果公司用 pnpm workspace,把 TS 版 Grunt 插件作为内部包,记得在根目录 pnpm-workspace.yaml 里声明 "packages/*"否则类型解析路径会飘
  2. 性能优化:Grunt 任务默认同步执行,TS 编译后仍是同步,若任务重 IO,可内部改用 grunt.util.asyncPromise,但注册方式仍要保持回调风格,否则 Grunt 会提前结束。
  3. 未来迁移:一旦团队全面切到 Vite/Rollup,可把同一套 TS 源码改写成 Rollup 插件复用类型定义,实现“同一内核,双构建引擎”,降低二次开发成本,这也是国内大厂渐进式演进的常用套路。