1.2 与 JavaScript 的关系及互操作


1.2 与 JavaScript 的关系及互操作

这一节处在知识地图里"基础环境"和"类型系统原理"之间,职责是拆穿一个常见误解:TypeScript 是不是另一种语言?答案很干脆——它不是。理解这点,你才不会在混合代码库里踩到契约失效的坑。

类型擦除:编译后 TypeScript 不再是 TypeScript

TS 编译器做的事,本质是把带类型的 TS 翻译成纯 JS,顺手在翻译过程中检查类型是否自洽。类型标注本身不进产物。

// 这是 .ts 源码 interface Point { x: number; y: number; } function move(p: Point, dx: number): Point { return { x: p.x + dx, y: p.y }; } // 编译后(简化).js 长这样,interface 和 :Point 全没了 function move(p, dx) { return { x: p.x + dx, y: p.y }; }

输入:上面的 .ts。输出:下面的 .js,运行时没有任何类型检查。这意味着:如果有人传 { x: 1 }(缺 y)进来,运行时不报错,只有编译期那一道门拦过。契约的防护范围,严格限定在"经过 tsc 检查过的代码路径"。

渐进迁移:老 JS 项目怎么吃进 TS

现实里很少有项目从零写 TS,多数是在一个 JS 仓库里逐步替换。关键开关是 allowJscheckJs

// tsconfig.json 片段:允许 JS 与 TS 混编 { "compilerOptions": { "allowJs": true, "checkJs": false, "outDir": "dist" } }
// legacy.js —— 即使 checkJs 关着,TS 也能 import 它,只是不检查内部 // @ts-check // 在文件头加这一行,可单独开启该文件的检查 /** * @param {number} a * @param {number} b * @returns {number} */ function add(a, b) { return a + b; }

背景:团队有 300 个老 .js 文件,不可能一天改完。操作:allowJs: true 让新旧共存,再对关键文件加 // @ts-check 逐个收紧。结果:迁移期间两套代码并行,不阻断业务。解读:这是契约的"渐进签署"在工程层的体现。变式:用 // @ts-nocheck 可临时整文件跳过检查,应急用,别常驻。

互操作边界:契约在哪里会失效

混合编程最危险的地方,是类型信息在边界"断崖"。下面几种情况,TS 的静态保证会漏:

// 1) 从 JSON 解析来的数据:类型是 any,契约没接住 const raw = JSON.parse('{"x":1}'); // raw 是 any function usePoint(p: { x: number; y: number }) {} usePoint(raw); // 编译通过,但运行时 p.y 是 undefined // 2) 第三方 JS 库没类型声明:只能 declare 成 any 形状 declare function oldLib(arg: any): any; // 3) 动态属性访问绕过检查 const obj: { a: number } = { a: 1 }; obj["b" as any] = 2; // 用 as any 强行绕过,编译器放行

输入输出:上面三段的 TS 编译都通过,但运行时 raw.yobj.b 都是隐患。这正是为什么现代项目在外部输入边界用运行时校验库补一道——TS 管编译期,zod 之类管运行期,两个门各有分工,别指望一个顶俩。

01-02-fig01-2

类型声明文件:JS 库如何获得契约

一个纯 JS 库,只要配一份 .d.ts,TS 项目就能获得完整类型提示,无需改库的源码。

// mathlib.d.ts —— 给没有类型的 JS 库补一份契约 declare module "mathlib" { export function sum(nums: number[]): number; export function mean(nums: number[]): number; } // 之后在 TS 里 import,参数和返回值都被检查 import { sum } from "mathlib"; sum([1, 2, "3"]); // 报错:'3' 不是 number

社区维护的 @types/* 就是这种声明的集合。绝大多数流行 JS 库都能在 DefinitelyTyped 找到对应声明。找不到时,自己写一份最小 .d.ts 也比 any 强——至少把边界形状钉死。

anyunknown:两个"放行口"的语义差

混合编程绕不开动态值,anyunknown 是两条不同严格度的出口。

let a: any = JSON.parse("{}"); a.foo.bar.baz(); // 编译放行,运行时炸 let u: unknown = JSON.parse("{}"); u.foo; // 报错:unknown 上不能直接访问属性 if (typeof u === "object" && u !== null && "foo" in u) { // 收窄后才能用,契约重新接上 }

any 等于"我放弃这段的契约",unknown 等于"我不知道,但你得先证明再动"。混合迁移早期用 any 推进度,稳定后逐处换成 unknown 加收窄,是更稳的节奏。

工程取舍:tsc 直译还是用 Babel?

纯 TS 项目用 tsc 一把梭最简单。但若团队已用 Babel 做转译(要快、要兼容旧浏览器),可让 Babel 只剥离类型(不检查)、tsc --noEmit 单独跑类型检查。两件事分开后,热更新快、类型检查可并行。

本节要点回顾

  • TS 编译 = 擦除类型 + 翻译为 JS,类型不进产物
  • allowJs/checkJs/@ts-check 是渐进迁移的三件套
  • 外部输入、第三方 JS、动态访问是契约断崖高发区
  • .d.ts 给 JS 库补契约,unknown 优于 any

⚠️ 别以为"用了 TS 运行时就不会出错"。类型擦除后,非法输入照样进运行时,边界处必须另加校验。

💡 给老 JS 库补 .d.ts 时,先写你实际用到的几个函数签名,比抄全量 API 更划算,也更容易保持准确。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U