TypeScript 自测题 · 「了解 / 熟悉」两档分级
用途:判断简历「专业技能」栏里 TypeScript 该写了解还是熟悉。 设计原则:不是通用题库,而是按面试官出题口径分级 —— 你写哪个词,对方就问到哪个深度。答不上来的那一档,就别写那个词。
最后更新:2026-10-07
0. 怎么用(三步)
- 先做第一档(30 题),不看答案,把不确定的题号圈出来。
- 再挑第二档(25 题),只做「有印象但说不清」的,不必硬做完全不会的。
- 对照第 1 节的定档规则,决定简历里写哪个词。
自测纪律:答的时候要能出声说出来。心里觉得"我懂"但嘴上组织不出来,面试现场就是答不上来 —— 面试考的是表达,不是眼熟。
1. 定档规则
| 第一档正确率 | 第二档正确率 | 简历怎么写 |
|---|---|---|
| < 50% | — | 别写 TS。先补基础,写了反而是给自己挖坑 |
| 50% – 70% | — | 可写「了解」,但面试前必须把错的部分补掉 |
| ≥ 70% | < 30% | ✅ 写「了解」(这是绝大多数 9 年前端的老 Vue2 老兵的正常位置,完全不减分) |
| ≥ 85% | ≥ 60% | 可以写「熟悉」 |
| 满分 | ≥ 85% | 可以写「熟练」 |
两个必须理解的判断:
- 写「了解」不是谦虚,是给自己划定出题范围。 面试官按你写的层级校准难度:写「熟悉」他有资格问
infer、条件类型、类型体操;写「了解」他只会问interface和type的区别、any/unknown。 - 风险不对称:9 年经验写「了解 TS」没人会因此看低你;但写「熟悉」却没答上来,会连带拖累你「熟悉 Vue2」那些话的可信度。前者零成本,后者高代价。
2. 第一档「了解」· 30 题
只要简历里出现 TypeScript 这个词,这一档全部要能答。
2.1 类型基础(1–10)
1. 用一句话说清:TS 和 JS 是什么关系?类型在运行时还存在吗?
2. TS 能给你带来什么实际好处?(至少说三条)
3. any 和 unknown 有什么区别?为什么说 unknown 更安全?
4. never 在什么情况下出现?举两个例子。
5. interface 和 type 有什么区别?你什么时候用哪个?
6. void 和 undefined 作为函数返回类型有什么不同?
7. 数组 string[]、元组 [string, number]、联合 string | number 分别什么时候用?
8. readonly 修饰属性意味着什么?和 const 区别在哪?
9. 可选属性和可选参数怎么写??. 和 ?? 各解决什么问题?与 || 的区别?
10. 类型断言 as 和非空断言 ! 是什么?为什么很多规范要求尽量少用?
2.2 类型系统与推断(11–18)
11. 类型注解是不是每个变量都要写?TS 能自动推断到什么程度?
12. 什么是「类型收窄」(narrowing)?说三种触发收窄的写法。
13. typeof 在值位置和在类型位置含义有什么不同?举一个类型位置的例子。
14. 联合类型上的属性访问为什么会报错?怎么解决?
15. 交叉类型 A & B 是什么语义?和联合类型方向相反在哪?
16. 字面量类型('success'、1、true)有什么用?as const 是干什么的?
17. 枚举 enum 和字面量联合 'a' | 'b',你倾向用哪个?为什么?
18. 什么是结构化类型(鸭子类型)?它和 class 的 implements 有关系吗?
2.3 泛型与函数(19–24)
19. 泛型解决什么问题?不给泛型会有什么后果?
20. 泛型约束 <T extends ...> 是干什么的?举个你会用的场景。
21. 函数类型怎么标注?可选参数、默认参数、剩余参数的写法?
22. 箭头函数和函数声明的类型标注有什么细微差别?
23. 类的 public / private / protected / readonly 分别是什么意思?
24. 抽象类 abstract 和接口 interface 都能约束类,怎么选?
2.4 工程配置与编译(25–30)
25. tsc 干了什么?为什么 TS 代码要先编译才能跑?
26. tsconfig.json 里你会关注哪几项?(至少说四个)
27. strict: true 打开了哪些检查?关掉会怎样?
28. .d.ts 声明文件是干什么的?什么时候需要自己写?
29. 类型错误会让构建失败吗?你们项目是怎么处理的?
30. 接口返回的数据能靠 TS 类型保证正确吗?为什么?
3. 第二档「熟悉」· 25 题
只有简历写「熟悉」才需要过这一档。 写「了解」时对方基本不会问到这里。
3.1 类型编程(31–40)
31. keyof T 和 T[K](索引访问类型)各得到什么?
32. 映射类型 { [K in keyof T]: ... } 是干什么的?手写一个 Partial<T>。
33. 映射类型里的 +?、-?、-readonly 是什么意思?手写一个 Required<T>。
34. Pick、Omit、Record、Exclude、Extract、NonNullable 各是干什么的?
35. 手写 Exclude<T, U>,并解释为什么里面要用 never。
36. 条件类型 T extends U ? X : Y 怎么读?
37. 什么是「分布式条件类型」?什么情况下会触发分发?
38. infer 是干什么的?手写 ReturnType<T> 和 Parameters<T>。
39. 内置工具类型 Partial 为什么是浅层的?写一个 DeepPartial<T>。
40. 模板字面量类型是什么?举个你会用它的场景。
3.2 进阶收窄与类型安全(41–46)
41. 类型谓词 function isFoo(x: unknown): x is Foo 有什么用?为什么不能用 as 代替?
42. 断言函数 asserts x is Foo 和类型谓词有什么区别?
43. 函数重载怎么写?TS 5 里和泛型相比,什么时候必须用重载?
44. 什么是协变、逆变?为什么函数参数是逆变的?
45. strictFunctionTypes 管什么?为什么类方法的参数是「双变」的?这是不是不健全?
46. 如何给一个第三方库(没有类型的)写声明?declare module 怎么用?
3.3 声明、模块与扩展(47–51)
47. 什么是「声明合并」?interface 和 namespace 分别怎么合并?
48. .d.ts 里的 declare global 用来干什么?给 window 加属性的正确姿势?
49. 模块扩充(module augmentation)是什么场景?
50. import type 和 import 有什么区别?为什么要区分?
51. TS 5 的标准装饰器和老的 experimentalDecorators 有什么不同?Vue3 / Angular 里用的是哪个?
3.4 工程实践与框架结合(52–55)
52. 类型在运行时不存在 —— 那接口数据怎么做校验?你了解哪些方案(如 zod)?
53. Vue3 + TS:defineProps 的泛型写法、withDefaults、defineEmits 怎么带类型?
54. React + TS:泛型组件怎么写?常用的事件类型、useState 泛型怎么标?
55. 复杂类型拖慢编译和 IDE 反应速度,你怎么排查和处理?
4. 答案要点(自评用)
要点覆盖到就是过。别背原文,能用自己的话说出来才算过。
第一档答案要点
| # | 要点 |
|---|---|
| 1 | TS 是 JS 的超集,在 JS 上加静态类型;类型只存在于编译期,编译后全部擦除,运行时没有类型(instanceof 是另一回事,那是 JS 原生机制)。 |
| 2 | ① 编译期发现错误(不用等运行时);② IDE 智能提示、跳转、重构安全;③ 类型即文档,接口契约明确;④ 大项目多人协作时降低沟通成本。 |
| 3 | any 是检查开关全关,可随意读写、能赋值给任何类型;unknown 是类型安全的 any —— 什么都能赋给它,但用之前必须先收窄。外部输入(接口返回、JSON.parse)应该用 unknown 而不是 any。 |
| 4 | ① 函数永不正常返回(总是 throw 或死循环);② 穷尽性检查(把联合类型所有分支处理完后,剩余分支可赋给 never,漏了就会编译报错);③ 类型运算中表示空集。 |
| 5 | ① interface 支持声明合并(同名自动合并),type 不支持;② type 能表达联合、元组、条件、映射、模板字面量,interface 只能描述对象/函数形状;③ 扩展方式:interface extends,type 用 &。实践:对象/类的公共契约用 interface,其余用 type。两者现代 TS 性能差异已很小。 |
| 6 | 返回类型 void 表示返回值不可用(调用方拿到 undefined 或忽略),undefined 是必须显式返回 undefined。回调函数返回类型通常标 void。 |
| 7 | 定长同类型用数组;定长且各位置类型不同用元组(如 useState 返回值、坐标);取值是几个之一用联合。 |
| 8 | readonly 是编译期只读,防止属性被重新赋值,深层的不会被保护(需 Readonly<T> / as const);const 管的是变量绑定的引用,不管属性内容。 |
| 9 | 属性后加 ?、参数后加 ?(可选参数必须在必选之后);?. 在左侧为 null/undefined 时短路返回 undefined;?? 只在左侧是 null/undefined 时取右值,\|\| 会把 0、''、false 也当假值 —— 这是表单/计数场景最常见的坑。 |
| 10 | as 强制告诉编译器"我知道它是什么"(不做运行时转换);! 断言非空。两者都会绕过编译器保护,一旦判断错就变成运行时报错;规范一般要求尽量避免,改用收窄或校验。 |
| 11 | 不必全写。TS 能从赋值、返回值、参数默认值、上下文推断出大部分类型。建议:函数返回值可以显式标注(契约清晰、错误更早暴露),局部变量交给推断。 |
| 12 | 收窄 = 把宽类型缩到窄类型。触发方式:typeof x === 'string'、if (x)、x instanceof Foo、'a' in obj、判别式联合(obj.type === 'a')、自定义类型谓词。判别式联合是中后台业务里最常用的。 |
| 13 | 值位置 typeof x 返回 JS 的字符串('string');类型位置 typeof obj 返回该变量的类型(如 typeof config 得到 config 的类型)。 |
| 14 | 因为联合类型上只有共有属性才能直接访问。解决:先收窄(判别式 / in / typeof),或写类型谓词函数。 |
| 15 | A & B 表示同时满足(取并集),A \| B 表示满足其一。方向相反:交叉越多越窄,联合越多越宽。 |
| 16 | 字面量类型把取值限制到具体值,常用于状态机、枚举式取值;as const 让对象/数组变成只读且字面量(不拓宽成 string),常用于配置表、常量表。 |
| 17 | 倾向字面量联合:编译后是普通字符串,无额外产物、体积为 0、易与后端对齐;enum 会生成运行时对象(数值枚举还有反查表)。前端接口层建议用字面量联合。 |
| 18 | TS 是结构化类型:只看形状是否兼容,不看名字,这就是"鸭子类型"。class implements 只是让编译器检查形状,implements 不会增加运行时内容。 |
| 19 | 泛型让类型成为参数,从而在保持类型信息的前提下复用逻辑。不用泛型就得写 any,丢失所有类型保护。典型:请求封装 request<T>(url): Promise<T>、ref<T>、列表组件。 |
| 20 | 限制 T 的范围,从而能在函数体内安全访问某些属性。例:function pick<T extends { id: number }>(x: T)。也可用来约束为某个键:<T, K extends keyof T>。 |
| 21 | (a: string, b?: number, c = 1, ...rest: number[]) => void。可选参数必须放必选之后;有默认值的参数会自动变可选。 |
| 22 | 箭头函数类型写在变量上;两者都要注意返回值类型标注的位置。泛型箭头函数在 .tsx 里要写 <T,>(x: T) => T(加逗号)避免被当成 JSX。 |
| 23 | public 默认;private 仅类内(编译期限制,运行时仍能访问);protected 子类可用;readonly 只能初始化时赋值。TS 的 private ≠ 真私有(#field 才是运行时私有)。 |
| 24 | 需要跨继承层次复用契约、或一个类要实现多个契约 → 用 interface(可多 implements);需要包含实现逻辑(模板方法)→ 用 abstract class(只能继承一个)。 |
| 25 | tsc 做类型检查 + 语法降级,把 TS 编译成目标 JS;类型只在编译期起作用,运行时不存在,所以必须先编译(或用 Vite/esbuild 等先转译再跑)。 |
| 26 | strict、target、module / moduleResolution、jsx、esModuleInterop、skipLibCheck、baseUrl / paths(路径别名)、include / exclude。paths 是日常最常动的(配 @/ 别名)。 |
| 27 | 打开 strictNullChecks(null/undefined 不再属于所有类型,这条价值最大)、noImplicitAny、strictFunctionTypes、strictPropertyInitialization、useUnknownInCatchVariables 等。关掉会大量丢失保护 —— 但老项目迁移时常常要分步打开。 |
| 28 | .d.ts 只放类型声明,不含实现,供编译器了解外部 JS 的形状。场景:给无类型的第三方库补类型、给全局变量/window 扩属性、给 *.vue、*.svg、import.meta.env 这类做模块声明。 |
| 29 | tsc 类型错误默认会阻断构建(noEmitOnError / 构建脚本里串 tsc --noEmit);Vite 默认不做类型检查,只在 vue-tsc / tsc --noEmit 步骤里查。这是常见误解点,答出来加分。 |
| 30 | 不能。 类型只是编译期的承诺,运行时不存在。接口返回的数据必须做运行时校验(zod 之类)或防御性取值,否则后端改字段前端照样崩 —— 而编译期一声不响。 |
第二档答案要点
| # | 要点 |
|---|---|
| 31 | keyof T 得到键名的字面量联合;T[K] 是索引访问类型,按键取值得到对应类型。两者常配合:T[K] 用在泛型里以保留具体类型。 |
| 32 | 映射类型遍历键生成新对象类型。type MyPartial<T> = { [K in keyof T]?: T[K] }。 |
| 33 | +? 加可选(默认)、-? 去可选、+readonly / -readonly 开关只读。type MyRequired<T> = { [K in keyof T]-?: T[K] }。 |
| 34 | Pick<T,K> 取子集;Omit<T,K> 去掉指定键;Record<K,V> 构造键值映射;Exclude<T,U> 从联合中剔除能赋给 U 的成员;Extract<T,U> 只保留能赋给 U 的;NonNullable<T> 去掉 null/undefined。 |
| 35 | type MyExclude<T, U> = T extends U ? never : T。用 never 是因为:never 与任何类型联合都会被消掉(X \| never = X),于是"剔除"就变成了"剩下的保留"。 |
| 36 | 读作「T 若能赋给 U,取 X,否则取 Y」。它是类型层面的 if;配合 infer 可以做类型提取。 |
| 37 | 当条件类型的被检查类型是裸类型参数且实参是联合类型时,会对联合的每个成员分别求值再合并。想要阻止分发,把两侧包起来:[T] extends [U] ? ...。 |
| 38 | infer 在条件类型里声明一个待推断的类型变量。type ReturnType<T> = T extends (...args: any) => infer R ? R : never;type Parameters<T> = T extends (...args: infer P) => any ? P : never。 |
| 39 | 因为是单层映射,嵌套对象只处理到第一层。type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] }。注意要处理数组和函数的情况,否则容易出问题。 |
| 40 | 在字符串字面量类型上做拼接/变换。例:从键名生成事件名 type Event = \on${Capitalize;配合Uppercase/Capitalize` 等内置工具做 API 路径约束。 |
| 41 | 告诉编译器「这个 unknown 经过我这个函数判断后就是 Foo」,从而在 if (isFoo(x)) 之后自动收窄。as 是无条件强制转换、不受保护;类型谓词是有条件的、且在函数体内可以真正做校验逻辑。写类型谓词时要保证判断逻辑真的对,否则是在骗编译器。 |
| 42 | 类型谓词用于 if 分支收窄;断言函数用于"不满足就抛错"的场景,调用后后续代码都被收窄(asserts x is Foo)。后者更适合做参数校验守卫。 |
| 43 | 多个签名 + 一个实现签名(实现在最后、签名要兼容所有重载)。当返回类型依赖于参数的具体形态、且用泛型表达会非常绕时,用重载更清晰。 |
| 44 | 协变:子类型关系保持方向(Dog 是 Animal → Dog[] 可赋给 Animal[])。逆变:方向翻转 —— 函数参数位置。理由:接收 Animal 的函数一定能处理 Dog,所以「能收 Animal」是「能收 Dog」的子类型关系。 |
| 45 | 管函数类型的参数是否做逆变检查。方法(对象字面量/类里的简写方法)的参数是双变的 —— 这是为了兼容早期写法而保留的不健全设计,所以 TS 的结构类型系统不是类型安全的(这叫 unsoundness)。知道这点说明真读过文档。 |
| 46 | 在项目里建 types/xxx.d.ts,用 declare module 'xxx' { export function f(): void } 补声明;把它放进 tsconfig 的 include(或 typeRoots)。更好的做法是找社区 @types/xxx。 |
| 47 | 同名 interface 自动合并成员(可用它给已有接口加字段);namespace 同名也会合并(用于给函数/类挂静态成员)。type 不能声明合并 —— 这也是 interface 在库设计里更常用的原因。 |
| 48 | 在 .d.ts 里用 declare global { interface Window { myApi: MyApi } };注意该文件必须有 import/export 才是模块,否则 declare global 会报错。 |
| 49 | 给第三方库的类型打补丁:declare module 'some-lib' { interface Options { extra?: string } },或扩展 Vue 的 ComponentCustomProperties。常用于给库加字段而不改库源码。 |
| 50 | import type { Foo } 只导入类型,编译后完全擦除,不产生运行时导入、不会造成循环依赖。配合 verbatimModuleSyntax / isolatedModules 时几乎是必须的。 |
| 51 | TS 5 的标准装饰器是 ECMAScript Stage 3 提案,签名与行为都不同((value, context) 形式);老的 experimentalDecorators 是 TS 自有实现,需在 tsconfig 打开。Angular 长期用老的;Vue3 的 <script setup> 基本不用装饰器(vue-class-component 时代才用)。 |
| 52 | 类型在运行时不存在,所以接口数据必须做运行时校验:zod / valibot / io-ts / ajv / 手写守卫函数。TS 官方推荐用 zod 之类做「一次性定义,同时得到类型和校验」。 |
| 53 | defineProps<{ id: number; list?: string[] }>();默认值用 withDefaults(defineProps<Props>(), { list: () => [] });defineEmits<{ (e: 'change', v: number): void }>()。注意 Vue3 里可选 props 带默认值后类型会自动去掉 undefined。 |
| 54 | 泛型组件:function List<T>(props: { items: T[]; render: (i: T) => ReactNode }) {},或 const List = <T,>(props: ...) => ...(.tsx 里加逗号)。事件类型 React.ChangeEvent<HTMLInputElement>;useState<User \| null>(null)。 |
| 55 | 用 tsc --extendedDiagnostics 看类型实例化次数和检查耗时;常见原因:深层递归类型、超大联合、复杂的条件类型链、any 造成的大范围推断、第三方类型本身有性能问题。对策:拆小工具类型、加上限、skipLibCheck、必要时用接口替代复杂类型。 |
5. 面试官怎么分辨你在哪一档
| 信号 | 说明 |
|---|---|
| 只问「有什么区别」 | 在测第一档。这一档必须全过。 |
| 让你手写工具类型 | 明确在测第二档,写不出来的话别说「熟悉」 |
| 问「你项目里怎么用 TS 的」 | 看你是真用过还是只背过概念 —— 准备一个具体例子 |
| 追问「为什么要这么写」 | 测理解深度。只会用不会解释 → 停在一档 |
| 让你看他给的报错信息并解释 | 最狠的一问。能解释清楚报错通常说明真用过 |
高分回答的两个特征
- 举自己的例子:「我在 xx 项目里用
Record<K, V>管理过状态映射」比背定义强十倍。 - 主动说边界:「这块我在生产里没用过,只了解概念」—— 面试官反而会信任你其余的回答。
6. 答不上来时的标准话术
别装懂。 装懂被追问两层必崩,而且会连带毁掉前面所有技能的可信度。
| 场景 | 话术 |
|---|---|
| 完全不会 | 「这块我了解概念,但没在生产里真正用过,不敢说会用。我的思路大概是从 X 入手,回去我会补上。」 |
| 有印象说不清 | 「我大致记得是用来做 X 的,但具体语法细节我记不准,不凭印象硬说。」 |
| 一档题答错 | 立刻补一句:「这个点我回去确认一下 —— 我平时更多是写业务类型,这类细节确实不熟。」 |
| 想把话题拉回来 | 「TS 我是从 Vue2 项目迁移时开始用的,实际最深的是在 xx 场景里处理接口类型 ——」(转到你有话说的领域) |
核心原则:承认不会不扣分;硬编扣大分。
7. 若决定写「熟悉」· 面试前必须能答的 Top 10
按被问概率排序,这 10 条能脱口而出,再考虑写「熟悉」:
any/unknown/never三者的区别与使用场景interfacevstype:区别 + 你怎么选- 泛型约束
<T extends ...>的实际用法(要能举项目例子) - 手写
Partial<T>和Required<T>(映射类型 + 修饰符) - 手写
Exclude<T, U>并解释为什么用never - 手写
ReturnType<T>(条件类型 +infer) - 判别式联合(discriminated union)在业务里的用法 —— 中后台最高频
strict系列打开了什么,strictNullChecks解决了什么问题- 接口数据为什么不能只靠类型保证,你怎么处理(运行时校验)
- Vue3 里
defineProps/defineEmits的泛型写法
8. 自测记录表
| 分档 | 题数 | 一次答对 | 答错/不确定 | 正确率 | 日期 |
|---|---|---|---|---|---|
| 第一档「了解」 | 30 | ||||
| 第二档「熟悉」 | 25 |
结论:简历里 TypeScript 写 → ☐ 了解 ☐ 熟悉 ☐ 熟练 ☐ 暂不写
提醒:把「了解」改成「熟悉」,只是一个词的事。等你把第二档过到 60% 以上,随时可以改 —— 到时候这个词就是白拿的。
posted on 2026-10-07 21:31 fox_charon 阅读(1) 评论(0) 收藏 举报
浙公网安备 33010602011771号