将独立注入式JS脚本迁移到TypeScript时return语句编译报错的问题咨询
问题描述
我正在将一批独立的注入式JavaScript脚本迁移到TypeScript,使用esbuild编译TS文件生成对应的JS脚本。这些脚本注入到页面时会被包装成AsyncFunction执行,所以原JS脚本中直接使用return语句来返回执行结果是合法的。但在TypeScript文件中直接写return时,编译器会报错:A 'return' statement can only be used within a function body.ts(1108)。
举个简单的例子,我的TS文件内容如下:
const input = document.querySelector('someSelector'); input.value = "someValue"; return true;
而注入脚本的代码逻辑是:
const scriptToInject = loadScript('someScript'); const globalVars = {}; const AsyncFunction = Object.getPrototypeOf(async function(){}).constructor; const func = new AsyncFunction(globalVars, codeToRun); const codeResult = await func(globalVars );
问题根源
TypeScript的静态类型检查会严格校验return语句的位置:它认为你的TS文件是全局脚本或ES模块,在这两种上下文的顶级作用域中,return语句是不允许的。但你的脚本实际运行时,会被动态包装进AsyncFunction的函数体内执行,这种运行时的上下文TS编译器无法感知到,因此产生了报错。
解决方案
针对这个问题,我提供几个从简单到规范的解决方案,你可以根据脚本的复杂度选择:
方案1:使用// @ts-expect-error注释忽略单个错误(推荐小型脚本)
这是最直接的解决方式:在return语句上方添加// @ts-expect-error注释,明确告诉TS编译器你预期这里会有一个错误,仅忽略这一行的校验。这种方式不会影响其他代码的类型检查,也不需要修改构建流程。
示例TS文件:
// 处理document.querySelector可能返回null的情况,避免TS报错 const input = document.querySelector('someSelector'); if (!input) { console.error("目标输入框未找到"); // @ts-expect-error: 运行时脚本会被包装进AsyncFunction,return合法 return false; } input.value = "someValue"; // @ts-expect-error: 运行时脚本会被包装进AsyncFunction,return合法 return true;
方案2:包装为函数+构建时提取函数体(推荐大型/复杂脚本)
对于逻辑复杂的脚本,更规范的做法是将代码包裹在一个函数中(让TS编译器通过校验),然后通过构建工具提取函数体的内容,最终输出的JS脚本仍然保持原有的“无函数包装”形式。
步骤如下:
- 将TS脚本内容放在函数中(支持async函数,适配你的AsyncFunction注入逻辑):
// script.ts async function injectedScript() { const input = document.querySelector('someSelector'); if (!input) { console.error("目标输入框未找到"); return false; } input.value = "someValue"; // 可以添加await逻辑,适配AsyncFunction await new Promise(resolve => setTimeout(resolve, 100)); return true; }
- 配置esbuild编译TS文件后,用一个简单的Node.js脚本提取函数体:
比如编写一个extract-function-body.js:
const fs = require('fs'); const path = require('path'); const inputPath = path.join(__dirname, 'dist/script.js'); const outputPath = path.join(__dirname, 'dist/injectable-script.js'); const code = fs.readFileSync(inputPath, 'utf8'); // 匹配函数体内容(适配async函数) const match = code.match(/async function injectedScript\s*\{\s*([\s\S]*)\s*\}/) || code.match(/function injectedScript\s*\{\s*([\s\S]*)\s*\}/); if (match) { fs.writeFileSync(outputPath, match[1].trim(), 'utf8'); console.log('函数体提取完成,输出文件:', outputPath); } else { console.error('无法匹配函数体,请检查输入文件'); }
- 在构建流程中,先执行esbuild编译TS,再运行这个提取脚本,最终得到的
injectable-script.js就是可以直接注入的内容。
方案3:使用// @ts-ignore注释(不推荐)
和@ts-expect-error类似,但@ts-ignore会忽略注释行之后的所有错误,风险更高,可能会掩盖其他潜在的类型问题,因此不建议使用。
示例:
const input = document.querySelector('someSelector'); input.value = "someValue"; // @ts-ignore return true;
额外的类型优化建议
为了让TS脚本更健壮,你还可以:
- 对
document.querySelector的返回值做非空校验(如方案1中的if (!input)),或者使用非空断言!(仅当你确定元素一定存在时); - 为DOM元素添加类型断言,比如
document.querySelector('input') as HTMLInputElement,这样TS会自动提示value等属性,避免类型错误。
示例:
const input = document.querySelector('input#search') as HTMLInputElement; if (!input) { // @ts-expect-error return false; } input.value = "TypeScript注入脚本"; // @ts-expect-error return true;
备注:内容来源于stack exchange,提问作者starsoccer

