ATRULE 技术博客ATRULE 技术博客
首页
博客
文档
关于
首页
博客
文档
关于
  • 技术文章

    • VuePress 2.x 完全指南
    • Gemini3 & GPT5 & DeepSeek3:AI时代程序员的身份转变
    • Agent 架构选型指南:单智能体 vs 多智能体
    • Agent 工程师转型学习路线图 (Full-Stack to Agent Engineer)
    • Agent 工程师学习笔记
    • 📖 Agent 系统架构与工程实践面试速记卡片
    • Agent 流式对话前端踩坑记录
    • RAG 检索工程问题记录
    • Agent 后端问题记录(Koa / MongoDB)
    • LLM 调用与 Agent 编排问题记录
    • Electron 桌面端问题排查记录
    • AI Agent 全栈开发问题排查记录
    • ES2026 与 TypeScript 进阶问题记录
    • 浏览器渲染与 V8 内存问题记录
    • React 19 与 Vue 4 新内核问题记录
    • 前端工程化与微前端问题记录
    • 性能、安全与可观测性问题记录
    • 前端架构实战问题记录
  • 项目实战

    • Node.js + Koa 抖音直播弹幕Agent 五大核心模块(面试项目完整版,TS技术栈)★★★★★
    • Poiclaw 项目蓝图:自主编程实体
    • AI旅行助手

ES2026 与 TypeScript 进阶问题记录

记录语言内核层踩过的坑:ES 新特性边界、TS 类型体操的实际事故,每个问题都是实际遇到过并解决掉的。

问题一:手写 DeepReadonly 把函数撕成了对象

现象:内置 Readonly 只处理一层,自己写的递归版遇到函数类型字段直接烂掉——函数变成 { apply, call, bind... } 的对象类型。

原因:mapped type 对所有键展开,函数类型也被撕开递归。

解决:分支顺序是答案——先拦原始值和函数,再拦 Map/Set、数组,最后才走对象映射:

type DeepReadonly<T> =
  T extends Primitive | Function ? T
  : T extends Map<infer K, infer V> ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>
  : T extends Set<infer V> ? ReadonlySet<DeepReadonly<V>>
  : T extends readonly (infer U)[] ? readonly DeepReadonly<U>[]
  : { readonly [K in keyof T]: DeepReadonly<T[K]> };

UnionToIntersection 依赖分布式条件类型:(U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never——U 必须是裸类型参数才分发。

问题二:Promise.resolve(syncFn()) 的同步 throw 接不住

现象:.catch 链挂在 Promise 上,但 syncFn 的同步 throw 还是变成 uncaught error。

原因:Promise.resolve(syncFn()) 里 syncFn 在进入 Promise 世界之前就执行了,同步 throw 根本不走 reject 路径。

解决:用 ES2026 的 Promise.try,把同步 throw 和异步 reject 统一进 reject 路径:

const promiseTry = <T>(fn: () => T | PromiseLike<T>): Promise<T> =>
  new Promise((resolve) => resolve(fn()));

// 封装"可能同步返回、可能异步拉接口"的工具函数
promiseTry(tool).then(trim).catch(handle) // 一条 catch 链搞定两种异常

问题三:TS 5 升级后装饰器全红

现象:关掉 experimentalDecorators 走标准版后,写了 5 年的方法装饰器签名报错。

原因:标准版方法装饰器签名从 (target, key, descriptor) 变成 (target, context)——没有 descriptor 了,直接改 prototype 的写法不再合法。

解决:通过返回替换函数拦截方法,元数据用 context.metadata:

function timed(target: (...args: any[]) => Promise<any>, context: ClassMethodDecoratorContext) {
  return async function (this: any, ...args: any[]) {
    const start = performance.now();
    try { return await target.apply(this, args); }
    finally { console.log(`${String(context.name)} ${(performance.now() - start).toFixed(1)}ms`); }
  };
}

注意求值自上而下、调用自下而上;老项目迁移按装饰器种类逐个改,类装饰器最平缓。

问题四:泛型入参类型被拓宽成 string

现象:defineConfig({ mode: 'dev' }) 推出来 mode 是 string,每个调用点写 as const 又太脏。

原因:默认推断对对象字面量属性做拓宽。

解决:TS 5.0+ 的 const 类型参数,调用点零成本保留字面量:

function defineConfig<const T extends Record<string, unknown>>(config: T): T { return config; }
// 推出 { mode: 'dev' } 而不是 { mode: string }

// 进阶:模板字面量类型从路由字符串推导参数
type ExtractRouteParams<T extends string> =
  T extends `${string}:${infer P}/${infer Rest}` ? { [K in P | keyof ExtractRouteParams<Rest>]: string }
  : T extends `${string}:${infer P}` ? { [K in P]: string } : Record<string, never>;
// parseRoute('/user/:id/post/:postId') → { id: string; postId: string }

问题五:Records & Tuples 嵌套普通对象直接抛错

现象:想用 #{a: 1} === #{a: 1} 的结构相等做缓存 key,嵌套一层普通对象直接 TypeError。

原因:RT 只能递归包含原始值或其他 RT——值语义的前提是内容完全静态,函数/class 实例/DOM 引用都进不去。

解决:写 deepToRecord 逐层转换(Date/函数节点报错而不是静默丢);应用点收窄到状态快照比较、memo props 比较、Map 的对象 key。它不是"不可变 JS"的银弹。

Prev
AI Agent 全栈开发问题排查记录
Next
浏览器渲染与 V8 内存问题记录