TypeScript 面试题库
收录真实面试常见问题,答案长度适合口头表达;拿不准时可返回相关主题复习。
基础
7个问题 · 0 已看01 什么时候应显式写 TypeScript 类型注解,而不是依赖推断? 查看答案 ▾ 收起 ▴
我会在需要表达或固定契约的边界写注解,例如公开函数的参数与返回值、导出对象、空集合,以及初始值比后续状态更窄的变量。实现内部的明显局部值通常交给初始化表达式和上下文推断。给每个常量重复标注 string 或 number 只会增加噪声。关键判断是:修改实现时,公开类型是否可以随之改变。如果不可以,显式注解能固定意图,并让编译器发现意外的 API 漂移。
02 在外部数据边界上,unknown 与 any 有什么区别? 查看答案 ▾ 收起 ▴
两者都能接收任意传入值,但带来的义务相反。any 会退出检查,属性访问、调用和赋值无需证明即可继续,不安全类型还会传播给调用方。unknown 则保留“不确定”这一事实,代码必须先收窄或验证,才能执行依赖具体类型的操作。对于 JSON、消息、环境变量派生值或第三方回调,我会先接收 unknown,检查容器和每个必填字段,再构造领域值。any 只用于有说明的迁移接缝,并把范围压到最小。
03 TypeScript 枚举会生成什么 JavaScript,数字枚举和字符串枚举有何不同? 查看答案 ▾ 收起 ▴
普通枚举会在运行时创建对象,这与接口和类型别名不同。字符串枚举输出从成员名到字符串值的属性;数字枚举还会输出从数字到名称的反向属性,所以 Object.keys 和 Object.values 会包含两个方向。重复数字值共用一个反向键,后声明的成员名会覆盖前者。构建配置会影响行为时,我会检查真实输出;遍历数字枚举时则按运行时值类型过滤,不会断言对象中的每个条目都对应一个声明成员。
04 什么时候应选择枚举、as const 对象或字面量联合? 查看答案 ▾ 收起 ▴
我会先看运行时契约。调用方只需要封闭类型,而且应该直接传原生字符串字面量时,字面量联合最简洁。as const 对象再增加一个可遍历的 JavaScript 值,并从同一事实来源派生联合,适合线上值和选项列表。API 有意要求枚举成员身份,或确实需要数字反查时,我会选择普通枚举。我不会根据笼统的包体积说法决定,因为输出和 tree-shaking 取决于实际编译器与打包器。无论采用哪种表示,外部输入仍需运行时验证。
05 ES 模块与 TypeScript 命名空间有何不同,应用代码应使用哪一种? 查看答案 ▾ 收起 ▴
ES 模块通过显式 import 与 export 建立边界,并参与运行时模块加载。TypeScript 文件只要含有顶层 import 或 export,就具有独立模块作用域;仅写 export {} 也足够。命名空间是 TypeScript 的名称分组机制,通常生成或描述一个运行时对象,并支持声明合并。现代应用和包边界应优先使用 ES 模块,因为加载器、打包器和 package exports 都理解它。命名空间仍适合部分全局脚本声明与声明合并模式。import type 只能提供类型位置所需信息,因为它会被擦除,不能提供运行时值。
47 TypeScript 的结构类型与多余属性检查有何区别? 查看答案 ▾ 收起 ▴
在 TypeScript 6 中,对象兼容性主要按结构判断:只要值具备目标要求的成员,即使还有额外成员,通常也可以赋值。直接出现在带目标类型位置的对象字面量还会接受一次多余属性检查,常能发现拼错的键;若先把同一对象存进变量,普通结构赋值规则可能不再报告该问题。这个过程不会删除属性,也不会验证运行时输入。常见陷阱是把字面量诊断当成精确对象保证;外部数据仍需解析,敏感输出仍应按允许列表显式构造。
50 为什么跨包版本发布 ambient const enum 有风险? 查看答案 ▾ 收起 ▴
TypeScript 6 默认会擦除 const enum,并在使用位置内联各成员值。因此,使用方可能用版本 A 编译并嵌入其数字编码,却在运行时加载编码已改变的版本 B,最终让实际分支与源码成员不一致。ambient 成员还会在 isolatedModules 的单文件编译模式下产生冲突。preserveConstEnums 可以为包内部保留运行时对象,但公开声明通常应去掉 const,或改用普通枚举、常量对象。这里的取舍应优先保证包边界稳定,而不是追求少量内联收益。
类型系统
15个问题 · 0 已看06 什么时候应使用字面量联合,而不是 string? 查看答案 ▾ 收起 ▴
当程序拥有一组有限且有明确含义的选择,例如请求方法、工作流状态或命令名称时,我会使用字面量联合。联合可以拒绝拼写错误、提供补全,并支持穷尽控制流。值由用户定义、可由服务器扩展或本来就是开放集合时,应继续使用 string。把 string 加进字面量联合不会兼得两种行为,因为 string 已经包含所有字符串字面量,约束会直接消失。外部边界即使使用封闭联合,也仍需运行时成员检查,因为 TypeScript 会擦除类型,而 JavaScript 仍能传入任意值。
07 控制字面量拓宽时,类型注解、as const 与 satisfies 有什么区别? 查看答案 ▾ 收起 ▴
类型注解声明变量或 API 边界应公开的类型,因此可以有意把具体初始化值拓宽为可复用联合。as const 要求检查器保留字面量值,并让对象字面量属性变为只读、数组字面量变为只读元组;它不会在运行时冻结任何内容。satisfies 检查表达式能否赋给目标结构,同时保留有用的推断细节,而不是用目标替换整个表达式类型。它既不添加只读行为,也不验证运行时数据。我会根据预期契约与修改模型选择,而不是看哪种写法能压掉错误。
08 TypeScript 的各类内置类型守卫分别能证明什么? 查看答案 ▾ 收起 ▴
typeof 会收窄 JavaScript 原始类型和函数,但 object 仍包含 null。instanceof 检查原型链中的运行时构造函数,因此不能验证普通 JSON,并可能对跨 realm 对象失败。in 只证明属性查找成功,包括原型链上的属性;它不证明属性值或所有权。相等比较证明特定值关系,字面量判别字段则收窄整个联合成员。我会选择与下一步操作所需事实一致的检查,再单独验证剩余字段约束。
09 为什么自定义类型谓词是一份双向契约? 查看答案 ▾ 收起 ▴
value is number 这类返回类型会影响两个结果:true 保留 number,false 则从当前联合中排除它。编译器会检查 number 与参数类型兼容,却不会证明函数体实现了这层关系。因此,只对小数字返回 true 的函数不能安全声明 value is number,因为大数字会进入检查器可能视为 string 的分支。我会测试合法成员、非法值,以及可能被错误拒绝的合法成员。若 false 不等于“不是 T”,就返回 boolean 或定义更精确的领域类型。
10 什么时候应依赖 TypeScript 推断,什么时候应写出注解? 查看答案 ▾ 收起 ▴
我会让编译器推断明显的局部值、有可靠上下文的回调参数,以及只属于单个实现的中间结果。若变化必须作为契约变化接受评审,就应写注解,例如公开参数与返回值、空集合、完整生命周期宽于初始值的状态,以及从无类型代码进入的数据。判断标准不是 TypeScript 能否推断,而是实现修改是否可以改变使用方看到的类型。发布代码时,我还会比较生成的 .d.ts,并保留正向与负向类型测试。
11 为什么抽取内联回调可能产生隐式 any 错误? 查看答案 ▾ 收起 ▴
内联回调在调用位置接受检查,而调用签名会提供上下文参数类型。如果先把箭头函数赋给无注解变量,该函数表达式会作为独立声明先接受检查,后续用途不会参与这一步。之后的 map、事件注册或 Promise 调用无法事后补回参数类型。在 noImplicitAny 下,参数会直接报错;没有该选项时,不安全的 any 还会在函数体内传播。我会保持短回调内联,或者给可复用函数写出预期参数与返回契约。
12 类型收窄中的声明类型与观察类型有什么区别? 查看答案 ▾ 收起 ▴
声明类型是变量在整个作用域中的赋值契约,观察类型则是控制流事实在某个程序位置允许的子集。一个 string | number 变量经过 typeof 检查后可观察为 string,后来赋值后又可观察为 number,但声明并未改变。路径汇合时,TypeScript 会合并每条可达路径的结果。这个区别可以解释为什么稍后赋入数字合法、赋入布尔值仍会失败,也能解释提前返回为何可以让剩余路径保持收窄。
13 可辨识联合与 never 如何实现穷尽性检查? 查看答案 ▾ 收起 ▴
联合类型的每个成员都带有同一个字面量判别字段,例如 status,并为该字面量配备专属载荷。switch 检查 status 时会在各个 case 中收窄整个对象。处理完当前所有成员后,剩余值是 never,因此默认分支可以把它传给 assertNever。新增成员会让剩余值变成具体成员而不再是 never,于是产生编译错误。这只检查静态封闭集合;JSON 数据进入联合类型前仍需运行时验证。
14 联合类型与交叉类型有何区别,这对赋值关系意味着什么? 查看答案 ▾ 收起 ▴
联合类型 A | B 接受可赋给至少一个成员的值,所以 A 可以赋给 A | B。读取联合值的代码在完成收窄前,必须对每个成员都安全。交叉类型 A & B 只接受同时满足两侧约束的值,因此它可以分别赋给 A 或 B。在 TypeScript 的结构类型系统中,这些关系是包含式的:一个字段足够丰富的对象可以同时满足两个联合成员。两个运算符都不会创建或合并运行时数据;它们只描述类型检查器接受什么,并会从 JavaScript 中擦除。
15 两个相交对象类型以不兼容方式声明同一属性时会发生什么? 查看答案 ▾ 收起 ▴
属性要求会继续交叉,而不是相互覆盖。因此,{ id: string } 与 { id: number } 组合后,id 的类型是 string & number;对普通值而言,这实际上是 never。& 不遵循 JavaScript 对象展开的覆盖顺序,也不会选出一个胜出字段。我会在组合前检查重名键。若领域模型要替换字段,就先对旧结构使用 Omit,再加入新声明,并显式执行运行时转换。若两个字段含义不同,就直接改名,而不是用类型断言压过冲突。
52 const 类型参数在什么情况下有助于字面量推断? 查看答案 ▾ 收起 ▴
在 TypeScript 6 中,const 类型参数会让推断优先为直接写在调用点的对象、数组和原始值字面量选择类似 as const 的候选类型。它适合元组工厂、路由定义和事件表,因为调用方写出的结构本身就是契约。它不会冻结运行时值,也无法从已经拓宽为 string 或 number[] 的变量中恢复字面量信息。取舍在于,过窄的只读结果会让后续修改变得困难;调用方需要宽泛、可变契约时,普通类型参数更合适。
56 断言函数与类型谓词有何不同? 查看答案 ▾ 收起 ▴
在 TypeScript 6 中,value is User 这类谓词返回布尔值,并同时收窄 true 与 false 分支;asserts value is User 这类断言函数则承诺只要正常返回,条件就已经成立,因此后续代码无需 if 也会收窄。它的失败路径必须抛出异常或以其他方式终止。只记录错误然后返回,是具体而危险的不健全写法。我会把谓词用于可恢复分支,把断言用于致命配置或不变量错误;若调用方需要多条验证信息,可辨识结果比这两种形式都更合适。
57 为什么空集合或初始 null 往往需要类型注解? 查看答案 ▾ 收起 ▴
在 TypeScript 6 中,空字面量提供的证据很少:空数组没有元素候选,{} 不会因后续写入自动获得已声明属性,而 null 也无法描述未来的 Connection。严格控制流分析可能根据 push 演化某些局部数组的观察类型,但这不是稳定的导出契约。我会在所有权边界标注预期元素类型或完整生命周期,例如 Job[]、Connection | null。代价只是多一点语法;相比之下,{} as Config 之类宽泛断言只会隐藏未完成初始化状态,并把错误推迟到更远的使用方。
58 TypeScript 什么时候会在闭包中保留类型收窄? 查看答案 ▾ 收起 ▴
在 TypeScript 6 中,如果参数或 let 变量已经完成确定的最后一次赋值,之后才创建非提升闭包,并且没有嵌套函数再给该变量赋值,编译器可以在闭包中保留收窄。若另一个闭包可能写入它,即使只是赋给自身,调用顺序未知也会让旧事实失效。我更倾向先验证并规范化,再捕获 normalizedUrl 这类 const,让证明边界一目了然。该规则不是并发保证;经过 await 后,共享可变对象仍可能通过别名改变,因此依旧需要所有权约束或复制。
59 为什么可辨识联合比多个彼此独立的联合字段更安全? 查看答案 ▾ 收起 ▴
在 TypeScript 6 中,{ format: "json"; payload: string } | { format: "binary"; payload: Uint8Array } 保留了字段关系:检查 format 后,相应的 payload 会一起收窄。若改写为 { format: "json" | "binary"; payload: string | Uint8Array },两个字段就会独立变化,从而允许错误组合。多个可选字段也有同样问题,会让非法状态可表示。可辨识联合需要显式写出更多变体,但能提供穷尽检查和更清楚的构造路径。它仍是静态契约,JSON 进入联合前必须按判别字段及对应载荷执行验证。
泛型与高级类型
17个问题 · 0 已看16 keyof、索引访问类型和映射类型如何协作? 查看答案 ▾ 收起 ▴
keyof T 会产生 T 的已知键联合,索引访问 T[K] 则取得一个键或一组键对应的值类型。映射类型遍历键联合,为每个键构造属性,并可修改修饰符、值类型或名称。常见模式是先把每个键映射成完整对象,再用 keyof T 索引映射结果,得到可辨识联合。相关字段都使用同一个 K 时,对应关系会保留;如果分别索引各字段,这种关系就会丢失。
17 为什么精确派生的 TypeScript 类型在数据边界仍可能不安全? 查看答案 ▾ 收起 ▴
TypeScript 输出 JavaScript 时会擦除类型,因此条件类型或映射类型不会执行运行时验证。JSON.parse(raw) as Event 只是要求检查器信任程序员,并不会检查判别字段、必填字段或数字范围。外部值应先以 unknown 进入系统,通过运行时解析或验证后才能获得领域类型。越过这个边界后,派生类型可以维护可信值之间的关系。还要审核连接 Object.keys、动态赋值与映射类型的断言,因为运行时枚举与 keyof 并不相同。
18 泛型类型参数能表达哪些 any 无法表达的信息? 查看答案 ▾ 收起 ▴
类型参数会在一次调用或一个实例的签名中连接多个位置。在
19 类型参数应放在方法上,还是放在接口或类上? 查看答案 ▾ 收起 ▴
如果每次调用都可以独立选择类型,例如转换器每次接收新的输入类型,就把参数放在方法上。如果一次选择需要在实例生命周期内约束多个成员,例如 Repository
20 怎样在 Pick、Omit 与单独声明的接口之间选择? 查看答案 ▾ 收起 ▴
我会根据来源变化应怎样传播来选择。Pick 是允许列表,来源新增字段不会自动进入结果,通常适合公开响应和受限命令。Omit 是排除列表,来源新增字段会自动流入,适合只去掉少量基础设施字段的内部结构。若两个契约只是当前相似,未来应独立演进,就应单独声明接口。两种工具都不会改变运行时数据,因此安全敏感的输出仍要按允许字段构造新对象,并测试实际序列化结果。
21 为什么 Partial<Entity> 通常不是良好的更新 API? 查看答案 ▾ 收起 ▴
Partial 只改变每个第一层属性能否缺省,不决定调用方有权更新哪些字段。它直接作用于持久化实体时,往往会暴露 ID、所有权、角色、审计字段和秘密值。它还是浅层转换,嵌套对象会整体替换,并不会自动成为定义良好的嵌套补丁。我会先用 Pick 建立明确的更新允许列表,再应用 Partial 或声明操作专用的可选字段。运行时还要拒绝未知键,定义缺省与清空语义,并在 exactOptionalPropertyTypes 下测试显式 undefined。
22 条件类型何时会对联合分布,怎样把联合整体作为测试对象? 查看答案 ▾ 收起 ▴
当受检查一侧是裸类型参数,例如 T extends U ? X : Y 时,条件类型会分布。若 T 是联合,TypeScript 会分别处理每个成员,再合并结果。因此 ToArray<string | number> 可以得到 string[] | number[],而不是 (string | number)[]。若要对完整联合只比较一次,应把两侧包装成 [T] extends [U]。测试时要同时覆盖混合联合与 never,因为对 never 的分布会直接产生 never,不会像普通成员那样进入某个分支。
23 生产级 DeepPartial 工具在递归前必须决定哪些边界? 查看答案 ▾ 收起 ▴
它必须先定义哪些结构可以继续遍历,哪些值应视为原子并保持不变。朴素的 T extends object 也会匹配函数、数组、元组、Date 和类实例,常会破坏调用签名或集合语义。我会先处理原始值与函数,再分别决定元组、数组和内置对象的规则,并写入契约。还要区分可选属性与值类型包含 undefined 的必填属性,尤其是在 exactOptionalPropertyTypes 下。最后应限制或简化递归,并测试联合、只读成员和本就可选的字段,使编译成本与输出可预测。
24 infer 在条件类型中做什么,模式不匹配时会怎样? 查看答案 ▾ 收起 ▴
infer 会在 extends 模式的某个位置引入类型变量。输入可赋给该模式时,TypeScript 捕获对应部分,并让该变量在真分支中可用。例如,T extends readonly (infer E)[] ? E : never 可以提取数组或元组的元素类型。模式不匹配时进入假分支;infer 不是运行时反射,绑定也不会逃出所属分支。处理元组时,我会用剩余模式保留位置关系;处理重载函数时,则要记住推断使用最后一个调用签名,而不是逐个解析所有重载。
25 键重映射与 never 如何在映射类型中过滤属性? 查看答案 ▾ 收起 ▴
映射类型会遍历一个键联合,通常是 keyof T,并为每个键生成属性。as 子句计算输出键;若计算结果为 never,该属性就被过滤,否则可以保留或通过模板字面量类型改写名称。值一侧仍可使用当前键,例如 T[K],因此按值类型过滤时仍能保持名称和值的对应关系。-?、-readonly 等修饰符前缀会分别改变可选性与可变性。这一切只发生在编译期,不会在运行时重命名或删除 JavaScript 对象的属性。
26 怎样让递归条件类型既正确又可处理? 查看答案 ▾ 收起 ▴
首先要有真实的基线分支,并保证每次递归都让结构变小,例如剥离一个元组元素或一层数组。还要明确联合是否应分布,以及函数、数组、元组和内置对象究竟是叶子还是容器。无限制的对象递归可能展开巨大联合、丢失元组细节,或触发类型实例化过深错误。公开工具通常应加入深度累加器,并在达到上限时返回写入契约的后备类型。测试要覆盖空元组、只读集合、联合、never 和自引用对象类型。运行时循环数据是另一问题,需要能识别环的遍历逻辑。
27 模板字面量类型如何派生字符串 API,它的适用边界在哪里? 查看答案 ▾ 收起 ▴
模板字面量类型会在编译期拼接字面量组成部分。占位位置若是联合,TypeScript 会生成笛卡尔积,因此 ${Locale}_${Message} 能派生所有允许组合。它与键重映射及 Capitalize 配合后,还能从一个来源联合生成 onUserCreated 一类名称。这个机制适合由程序拥有的有限集合。多个大联合会迅速相乘,拖慢检查并产生难读诊断;开放的用户字符串也得不到多少精度。该类型不会执行运行时解析,所以来自外部的路由、事件名和环境键仍需验证。
28 怎样判断一个泛型类型参数是否传递了有用信息? 查看答案 ▾ 收起 ▴
有用的类型参数至少会连接两个位置,或以调用方可观察的方式约束某个位置。在 <T>(value: T) => T 中,它连接输入与输出;在 <K extends keyof T> 中,它连接所选键与 T[K]。若参数只出现一次,通常应改为具体类型或 unknown,因为调用方选择它也得不到任何关系。约束只描述所需能力,不会转换值,也不会验证运行时数据。我会优先让调用点推断,仅在证据不足时显式传类型参数,并检查生成声明,确认默认值和约束没有扩宽公开契约。
29 怎样保留事件名与其载荷类型之间的对应关系? 查看答案 ▾ 收起 ▴
先建立唯一的事件映射,例如 { saved: Saved; failed: Failure },再把事件名声明为类型参数 K extends keyof Events。载荷位置使用 Events[K],于是每次调用会同时选择一个键及其对应值类型。若分别写 keyof Events 与 Events[keyof Events],对应关系就会丢失,错误事件也可能接收另一个事件的合法载荷。同一映射还能通过映射类型派生处理器属性,或为队列生成可辨识联合。动态名称和外部载荷在运行时仍需验证;泛型只维护已被检查器接受的值之间的关系。
46 怎样让公开的递归类型或条件类型保持可控? 查看答案 ▾ 收起 ▴
在 TypeScript 6 中,我会先定义支持的输入形状和真正的基线分支,再确保每个递归分支都让结构变小。测试应针对最终导出的别名,而不只测内部辅助类型,并覆盖正向赋值、@ts-expect-error 负向用例、联合、never 和支持的最深结构。如果诊断泄露多层分布细节,或编译触发类型实例化过深,就应命名中间类型、拆分阶段,或在边界返回具名领域类型。深度参数可以限制工作量,但其截断值是设计取舍,不能当作跨版本稳定的编译器上限。
51 为什么泛型函数执行某些操作时需要运行时见证? 查看答案 ▾ 收起 ▴
TypeScript 6 会擦除类型参数,所以函数不能计算 value instanceof T、直接调用 new T(),也不能通过检查 T 选择序列化器。它必须接收运行时证据:用构造器创建实例,用谓词或 schema 验证输入,或用标签、策略进行分派。泛型参数随后把这份证据与承诺的结果关联起来。具体陷阱是接收 (unknown) => value is T 后就相信它一定诚实,因为检查器不会验证函数体。应独立测试见证;如果已经没有需要保留的类型关系,就改用更简单的非泛型 API。
60 Record 什么时候表示完整查找表,什么时候键仍可能缺失? 查看答案 ▾ 收起 ▴
在 TypeScript 6 中,Record<ClosedKeyUnion, Handler> 要求覆盖每个已知键;索引已收窄到该联合后,读取也可以保持精确。Record<string, Handler> 描述的却是字符串索引签名,并不会在运行时创建所有可能属性。启用 noUncheckedIndexedAccess 后,未声明的字符串查找会增加 undefined,从而暴露这处缺口;该选项不属于 strict 伞形配置,需要主动开启。程序拥有的注册表应使用有限键联合,开放键则采用带缺失检查的查找或 Map。常见陷阱是宽泛静态类型制造了确定性假象,最终调用不存在的处理器。
配置与迁移
12个问题 · 0 已看30 声明文件能保证什么,哪些内容必须另行验证? 查看答案 ▾ 收起 ▴
声明文件为在其他位置实现的代码提供静态契约。它可以让 TypeScript 拒绝参数不兼容的调用,在编辑器中展示已声明成员,并维护类型表达的关系。但它不会创建函数、验证外部数据,也不能证明 JavaScript 确实导出了声明中的值。我会分别验证两部分:先运行覆盖合法与非法调用的类型测试,再导入打包后的运行时产物,执行同一个公开入口。只有名称、模块格式、可选结果和异步行为都与实现一致时,这份声明才可信。
31 怎样测试手写声明是否符合无类型 JavaScript 包? 查看答案 ▾ 收起 ▴
我会从打包后的包开始测试,而不是直接读取源码目录,因为文件清单与 exports 也是契约的一部分。正向测试导入每个公开入口并检查代表性的推断结果;负向测试用 @ts-expect-error 标记必须被拒绝的参数与结果,声明意外变宽后,失去对应诊断的指令就会报错。我会在包支持的使用方配置下运行 tsc,路径不一致时检查 traceResolution。最后还要执行同一批运行时导入,对照导出形状、缺失值、Promise 与异常。类型测试和运行时测试覆盖不同失败面,不能互相替代。
32 TypeScript 严格模式能保证什么,它的保证在哪里结束? 查看答案 ▾ 收起 ▴
严格模式会为 null、隐式 any、this、函数兼容性、类初始化、catch 变量、内置迭代器以及 bind、call、apply 启用一组保守的静态检查。源码操作缺少足够类型证据时,编译器会拒绝它。保证范围到编译器能够检查的代码为止;类型会从 JavaScript 输出中擦除,因此 JSON、网络响应、JavaScript 调用方、显式 any、断言和被抑制的诊断仍可能违反声明模型。我把严格模式作为可信类型代码的强默认值,并在每个不可信边界补充运行时验证。
33 strictFunctionTypes 如何影响回调兼容性,哪个例外最重要? 查看答案 ▾ 收起 ▴
对于函数属性,参数兼容性按逆变方向检查。接收 Animal 的处理器可以放在只会收到 Dog 的位置,因为它能处理调用方提供的每个值;只处理 Dog 的函数不能充当通用 Animal 处理器。为了兼容常见类和 DOM 层次,方法语法仍允许双变,所以接口方法可能放过不安全方向。我会为新的回调契约使用函数属性,并用较宽输入测试现有的方法式 API。返回类型遵循普通的协变方向,应与参数分开审查。
34 TypeScript 类型覆盖率衡量什么,分母是什么? 查看答案 ▾ 收起 ▴
type-coverage 工具统计标识符,而不是源码行。基本比率是检查器类型不为 any 的标识符数除以所选程序中的标识符总数。一个上游 any 沿属性、调用和回调传播时,可能产生多个未覆盖位置。分母取决于实际生效的 tsconfig、文件过滤器、allowJs、生成源码、编译器版本和工具版本。只有这些输入固定时,我才比较结果,并同时查看原始计数与详情,因为取整百分比可能隐藏小幅回退。
35 为什么 100% 类型覆盖率不能证明运行时类型安全? 查看答案 ▾ 收起 ▴
覆盖率报告检查器相信什么,而不证明这种相信符合运行时数据。错误接口、手写声明文件、非空断言或 JSON.parse(raw) as User 都能让标识符获得具体类型,却没有检查任何输入值。类型随后还会从输出的 JavaScript 中擦除。我把满分理解为当前规则没有发现受跟踪的逃生口,而不是健全性证明。外部值仍应以 unknown 进入,通过运行时验证,并接受字段缺失、原始类型错误、容器损坏和领域约束等边界测试。
36 怎样在不冻结交付的情况下,把大型 JavaScript 代码库迁移到严格 TypeScript? 查看答案 ▾ 收起 ▴
我会先建立可复现的构建与测试基线,再用 allowJs 让 JavaScript 与 TypeScript 共存;重命名文件前可通过 checkJs 或 JSDoc 提前暴露风险。迁移按小改动推进,优先处理依赖叶子和稳定边界;不可信输入标为 unknown,旧模块外包一层窄适配器。每个已转换区域使用更严格的配置或只升不降的错误预算。指标不能只数 .ts 文件,还要跟踪显式 any、断言、被抑制诊断和未检查边界。持续测试并检查实际产物,才能在增强编译器保证时保持运行时行为稳定。
37 TypeScript 项目引用会怎样改变 monorepo 的构建? 查看答案 ▾ 收起 ▴
项目引用把一个大型程序拆成显式的复合项目依赖图。使用方根据依赖项目输出的声明进行类型检查,而不是把所有依赖源码载入同一个程序。运行 tsc -b 时,编译器会按依赖顺序构建前置项目,并利用构建信息跳过仍然最新的工作。解决方案配置通常只含 files: [] 和 references。每个被引用项目必须满足复合项目对文件集合与声明输出的规则。引用描述的是 TypeScript 构建依赖,不等于运行时包解析,因此 workspace 清单、exports 和产物路径仍需另行对齐并测试。
38 应怎样选择 tsconfig.json 中的 module 与 moduleResolution? 查看答案 ▾ 收起 ▴
选择依据应是运行时与构建流水线,而不是偏好的源码语法。module 控制 TypeScript 保留或输出的模块形式,moduleResolution 则控制怎样查找说明符、package exports、扩展名和类型声明。Node 应用要采用与 Node 模式及包设置匹配的组合;交给打包器的代码则使用该工具支持的 bundler 模式。target 与 lib 是另外两项决策,分别涉及输出语法和可用全局类型。我会用 tsc --showConfig、必要时的解析跟踪,以及对产物的实际执行来验证,因为类型检查成功不代表运行时加载器一定同意。
49 为什么 .d.mts、.d.cts 与 package exports 必须匹配运行时入口? 查看答案 ▾ 收起 ▴
在 TypeScript 6 的 Node 风格解析中,.d.mts 描述 ESM 的 .mjs 入口,.d.cts 描述 CommonJS 的 .cjs 入口;普通 .d.ts 对应 .js 时还会受邻近 package 格式影响。每个导出子路径和条件都必须把使用方引向与实际运行时文件一致的声明。把同一份声明复制到所有目标,可能掩盖默认导出或 export = 不匹配。我会从打包产物建立最小 ESM 与 CommonJS 使用方分别测试。这样会增加一些构建配置,但能防止源码别名和互操作选项遮住发布错误。
54 为什么 strict: true 不等于最高强度的 TypeScript 检查? 查看答案 ▾ 收起 ▴
strict 伞形选项会随版本演进:TypeScript 6 已包含 strictBuiltinIteratorReturn,因此升级编译器即使不改配置,也可能新增诊断。但它仍不会自动启用 noUncheckedIndexedAccess、exactOptionalPropertyTypes 或 noImplicitOverride;每项保护不同契约,也可能带来迁移成本。下游配置还可能覆盖某个严格子选项。我会用 tsc --showConfig -p ... 查看最终配置,并让 CI 使用同一项目入口。常见陷阱是用断言压掉新问题,表面恢复绿色构建,却丢失了原本要获得的保护。
55 怎样把类型覆盖率做成稳定的 CI 棘轮? 查看答案 ▾ 收起 ▴
我会固定 TypeScript 6、type-coverage 版本、最终生效的 tsconfig、严格计数模式和纳入文件集合,再记录基线的分子与分母。CI 使用 --at-least 阻止下降;已经稳定的范围可以使用精确的 --is。原始计数和详情差异能发现被取整百分比掩盖的回退,排除项则必须纳入版本控制并接受审查。升级编译器或工具时,应在独立改动中重算基线。这样会增加升级维护成本,但若把计数政策变化混入业务改动,历史趋势就失去意义,也容易诱导逐处补注解,而不是修复最早的 any 来源。
模式
9个问题 · 0 已看39 TypeScript 品牌类型能提供什么保证,又不能提供什么保证? 查看答案 ▾ 收起 ▴
品牌类型把基础类型与标记相交,使结构相同的值变得不兼容。若 UserId 与 ProductId 使用不同品牌,经过检查的 TypeScript 代码就不能混用它们,但两者仍能作为基础类型使用。这项保证只存在于静态检查阶段,而且依赖受控的创建路径;标记在 JavaScript 中会被擦除,不会执行验证、存在性检查或授权。类型断言、any 和 JavaScript 调用方都能绕过它。因此,我把品牌解释为“某个命名构造函数检查过一项本地不变量”的证据,而不是关于该值所有业务事实的证明。
40 怎样为来自外部数据的品牌值设计安全构造函数? 查看答案 ▾ 收起 ▴
我会在信任边界接收 unknown,并在编码前写清完整不变量。对于正数 ID,通常需要检查原始类型、整数、正数和安全整数范围。只有成功分支包含返回品牌类型的断言;失败时则向调用方返回有用的错误、Result 或异常。我会一起导出品牌别名与构造函数,但把唯一符号键留在模块内部,也不会公开通用转换辅助函数。测试应覆盖有效数据、缺失值、错误类型、小数、边界值,以及契约中写明的每项条件。
41 satisfies 与类型注解有什么区别? 查看答案 ▾ 收起 ▴
类型注解会固定变量对外暴露的类型,因此读取和后续赋值都按照写出的契约处理。satisfies 则检查表达式能否赋给目标,并保留表达式经过上下文推断后的结果。这适合注册表:既能要求键完整、条目合法,又能继续派生精确名称或判别值。变量需要在目标的完整范围内变化,或导出 API 需要稳定公开类型时,我会使用注解。两种写法都不执行运行时验证。
42 satisfies 是否会在不影响推断的情况下保留所有字面量类型? 查看答案 ▾ 收起 ▴
不会。目标会先参与上下文类型推断,然后编译器才检查可赋值性。目标为 “GET” | “POST” 时,属性可以保留 “GET”;目标只是 string 时,属性通常拓宽为 string,数字属性也通常会拓宽。目标中的元组分支还能让数组字面量推断为元组。完成上下文推断后,satisfies 不会用整个目标替换结果,也不会补上省略的可选属性。我会查看编译器展示的实际类型,并用正向类型测试确认能力。
43 标准 TypeScript 方法装饰器如何在不改变声明 API 的前提下修改行为? 查看答案 ▾ 收起 ▴
标准方法装饰器接收原方法值和上下文对象,并可以返回兼容的替代实现。安全包装器必须保留原来的 this、参数元组、返回类型、异常和异步行为;调用原方法时应使用 apply 或 call,不能把它脱离接收者。上下文会提供成员名以及是否为静态、私有成员等信息,addInitializer 则安排实例或类的初始化工作。即使运行时代码新增成员,类型签名也不会自动公开它们。标准装饰器与旧式 experimentalDecorators 及其元数据模型不同,库必须明确要求哪套契约。
44 模块增强要在运行时可靠,必须满足哪些条件? 查看答案 ▾ 收起 ▴
增强中的模块说明符必须解析到原声明所在的同一模块,而且新增类型成员必须描述运行时真实存在的行为。模块增强只合并声明,不会安装原型方法,也不会自动执行插件。若补丁由副作用导入完成,该导入必须在调用方使用成员前执行。增强可以修补现有声明,但不能新增顶层导出,也无法按名称增强默认导出。我会确保增强文件进入编译范围并导入原模块,同时测试类型检查以及运行时安装顺序。
45 多个装饰器工厂按什么顺序运行,这为什么重要? 查看答案 ▾ 收起 ▴
若装饰器从上到下写成 @first()、@second(),工厂表达式会自上而下求值,但得到的装饰器会自下而上应用,整体类似 first(second(method)) 的函数组合。包装器用于日志、缓存、授权、重试或异常转换时,顺序会改变外层实际观察到的调用与结果。我不会依赖装饰阶段的副作用,而会记录预期包装顺序,并用一次真实调用测试 this、参数、返回值、Promise 拒绝和同步异常。还必须确认项目采用标准装饰器,还是旧式 experimentalDecorators 签名。
48 品牌值经过转换后,什么时候必须重新验证? 查看答案 ▾ 收起 ▴
TypeScript 6 把品牌建模为交叉类型,因此品牌值仍可使用基础类型的操作,但这些操作不会自动保留证明。品牌数字参与算术后得到普通 number,序列化会丢失品牌,而返回基础类型的包装函数也会把它拓宽。只要操作可能破坏范围、格式、精度或更新后的验证规则,我就会重新调用具名构造函数。恒等式泛型可以在静态层面保留品牌,但只有运行时行为确实不变时,这份签名才可信。若不变量跨越多个可变字段,封装对象通常比品牌更合适。
53 怎样验证使用 satisfies 声明的注册表契约? 查看答案 ▾ 收起 ▴
在 TypeScript 6 中,我会使用项目真实的 tsconfig 运行类型测试,而不只查看编辑器悬浮结果。正向用例要实际消费保留下来的细节,例如特定 method 字面量或 keyof typeof registry;负向用例则用 @ts-expect-error 固定缺失键、多余键、非法值和禁止的重新赋值。注册表若被导出,还要检查生成声明,因为过窄的实现细节可能变成公开 API。这些检查只覆盖编译器行为,动态名称仍需运行时解析。常见陷阱是使用过宽的 Record<string, ...>,从而悄悄放弃有限键完整性。
没有符合筛选条件的问题。