三块面板:TS 源码 / 写死的 tsc 报错样本(非现场运行)/ 可点跑的等价 JS。核心分清:tsc 同时做“类型检查”和“转译”两件互不干涉的事,现代项目常把转译交给打包器、检查交给 tsc --noEmit。下面第三块面板的 JS 是真实的等价转译,可点着跑。
function add(a, b) { return a + b } // noImplicitAny:a、b 隐式 any
let u: { name?: string } = {}
return u.name.length // strictNullChecks:name 可能 undefined
const el = document.querySelector('#x')
el.addEventListener('click', () => {}) // el 是 Element | null,直接 . 触发空检查
strict:true 一次打开 noImplicitAny / strictNullChecks 等一串闸门。这几条错误正是它们各自的产物——把「隐式 any」「可能为空的值直接用」从运行时惊喜变成编辑期契约。老项目从 false 往 true 迁,常见过渡是逐个单开 noImplicitAny、strictNullChecks 再收敛,最后统一 strict:true。
{
"target": "ES5", // 语法降到 ES5:可选链 ?. 会被降级成三元判断
"lib": ["ES2020", "DOM"], // 声明“环境里有 ES2020 的 API”,Promise 才不报“找不到”
"strict": true,
"noEmit": true // 只检查不产出,转译交给打包器
}
// 陷阱:target ES5 + 代码用了 Promise,编译不报错、产物也不含 Promise,运行时 Promise is not defined
{
"include": ["src/**/*.ts", "src/**/*.d.ts"], // 哪些文件参与检查
"exclude": ["node_modules", "dist"], // 范围外仍可被 import,但不当入口检查
"paths": { "@/*": ["src/*"] }, // 别名:只影响类型解析,不影响运行时!
"noEmit": true, // 只检查不产出,转译交给打包器
"incremental": true // 缓存上次结果,第二次只重查变动文件
}
@/utils/foo 能解析到类型,但运行时得靠打包器(Vite 的 resolve.alias)或 Node 的 exports 真正解析——两边都要配,否则“IDE 能用、上线找不到模块”。这也回到第一章:检查(tsc)与转译(打包器)是两拨工具。