三块面板:TS 源码 / 写死的 tsc 报错样本(非现场运行)/ 可点跑的等价 JS。这些坑几乎都源于第一章两件事:类型会被擦除、检查与转译互不干涉。

一、坑:any 顺着返回值泄漏

TS 源码

const data = JSON.parse(text)     // JSON.parse 返回 any
data.user.profile.name            // 一路都不报错,但每个点都可能运行时崩
(这条在 tsc 里不报错——这正是坑:any 让编译器全程沉默) 建议改成: data.ts:1:7 - error TS2345: 若开了 noImplicitAny 且未标注,把 any 转成 unknown 再收窄才安全
为什么“没红”才是最危险的:any 会沿数据流传染,把整条链的类型抹平。对策是在边界写返回注解、把 any 尽快转成 unknown 再收窄。

等价 JS(去掉类型后可真跑)

[迷你控制台]

二、坑:结构化类型导致的「假兼容」

type UserID = string
type OrderID = string
function getUser(u: UserID) {}
getUser(orderId)   // ✓ 编译通过——两个都是 string,TS 不区分语义
(这条也不报错——TS 看形状不看名字。想区分要给品牌字段: type UserID = string & { readonly __brand: 'User' } 此时传 OrderID 才会报 TS2345)
为什么报不了:结构化类型只比形状。同为 string 的不同含义,编译器一视同仁——别指望它帮你防“把订单号当用户号”。

三、坑:过度断言 / 把 unknown 用 as 吃掉

catch (err) {
  const msg = (err as MyError).message   // 用 as 把 unknown 一步“吃”成想要的类型
}                                        // 若抛的不是 MyError,运行时 msg 是 undefined

四、坑:谁转译谁检查 / isolatedModules

export { SomeType } from './types'        // ✗ isolatedModules 下:SomeType 只是类型
export type { SomeType } from './types'   // ✓ 纯类型重导出必须用 export type

// esbuild/SWC 一个文件一个文件独立转译,拿不到全局类型图,
// 因此“本地能跑”≠“类型没问题”:完整检查得靠 CI 里的 tsc --noEmit
为什么:打包器只转译不检查,tsc 才检查。开了 isolatedModules 后,重导出类型必须写成 export type,否则单文件转译时无法判断它到底是值还是类型——这正是「类型擦除」与「独立转译」之间的必然要求。