前两天帮团队筛前端候选人,简历里写着“熟练使用 TypeScript”的十有八九,可真聊到细节,不少人卡在同一个地方:能写类型注解,但说不出 TypeScript 类型检查的本质、为什么需要它、它和 JavaScript 到底在“哪些层面”不同。前两天面到一个让我印象很深的例子,候选人答得很流利,把“TypeScript 是 JavaScript 的超集,多了类型系统,最终会被编译成 JavaScript”这套标准答案背得滚瓜烂熟。但当我追问“类型系统到底解决了什么问题,编译器做了什么,类型会不会影响运行时”时,对方就开始绕圈子了。
TypeScript 相关面试题在现阶段几乎躲不掉。不管是纯前端、偏 Node 的工程化方向,还是低代码、编辑器、基础设施这类重类型场景,只要团队日常用 TS,面试官想考察的绝不是你能背多少语法,而是你是否建立起了“类型思维”:怎么用类型表达业务约束,怎么理解编译期的价值,怎么在真实项目里规避类型失控。这篇汇总就是我结合这几年参与面试和日常维护 TS 项目的经验做的梳理,主线不绕弯:先讲清楚 TypeScript 和 JavaScript 的本质区别,再把高频的类型系统考点和面试题背后的原理拆开,然后过一遍泛型与工具类型的常见考法,工程配置相关考点放在后面,最后补上实际项目里容易踩的坑和面试官视角的建议。内容既适合马上要面试的人临时抱佛脚,也适合已经用 TS 写了半年一年但总觉得“哪哪不对”的同学。
需要提前说明的是,TypeScript 的知识点不是孤立的语法碎片,它背后是一整套“如何让代码在运行前就被校验”的设计哲学。你把这篇里的题目全背下来,不如把每个题背后的为什么想透,后者才是面试官真正愿意给高分的东西。
1. TypeScript 和 JavaScript 的区别:别把答案停在“超集”上
“TypeScript 是 JavaScript 的超集”这句话本身没错,但面试时只丢出这一句,基本等于没答。面试官真正想听的是:你在什么场景下感受到了二者差异,差异背后的机制是什么。
JavaScript 本身一直有类型。你用 typeof 能查出 "number"、"string"、"boolean"、"function"、"object" 这些运行时类型。问题是,JavaScript 的类型是在运行那一刻才确定的,而且它的类型检查近似于“放任自流”:
js复制function getCartTotal(items) {
return items.reduce((sum, item) => sum + item.price * item.count, 0);
}
getCartTotal("hello");
这段代码的问题在运行时才暴露。如果传入的不是数组,reduce 直接抛错;如果数组元素的 price 或 count 不是数字,最后的总价会悄悄变成 NaN,排查起来非常痛苦。TypeScript 做的核心事情,不是给 JavaScript “增加类型”,而是在运行之前多出一个静态分析阶段,让编辑器、编译器能提前读懂代码里哪些调用是非法的。
ts复制interface CartItem {
price: number;
count: number;
}
function getCartTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.count, 0);
}
getCartTotal("hello"); // 编译期直接报错
所以你面试时可以这样组织答案:TypeScript 的本质是“给 JavaScript 这套单飞了很久的类型体系,加了一层编译期约束”。它在语法上是 JS 的超集,在运行时不改变 JS 的语义,但它改变了代码的编写和审查方式。
1.1 “类型可擦除”意味着什么
TS 里绝大多数类型语法,比如 interface、type、各种泛型、类型注解,最后都会被编译器“擦掉”。getCartTotal(items: CartItem[]): number 编译成 JavaScript 后只剩 function getCartTotal(items) { ... }。这意味着 TypeScript 的类型检查基本不会给运行时增加额外开销——它跑在你写代码和构建的时候,不在用户浏览器里跑。
理解这一点对回答“TS 会改变运行行为吗”这种追问很有帮助。需要补一句:类型不是运行时的“安全网”。类型检查只对代码内部能看见的表达式负责,从外部过来的数据——比如用户输入、接口返回、localStorage 内容——它没法在你运行前就知道对不对。这也是为什么很多项目要配合运行时校验库。关于这一点,后面实战部分我会展开。
1.2 结构化类型系统:和 Java/C# 不一样的“兼容”标准
很多人学了 TypeScript 后,会潜意识里拿 Java、C# 的思维去理解它的类型,这是面试里容易露怯的点。TS 的类型兼容走的是结构化类型,也叫鸭子类型:只要“形状”一致,两个互不相干的类型就是兼容的。
ts复制interface Point {
x: number;
y: number;
}
function draw(p: Point) {}
draw({ x: 1, y: 2 }); // ok,对象字面量
draw({ x: 1, y: 2, z: 3 }); // 变量传对象时,多余的属性不会报类型错误
这带来一个很大的好处:mock、测试、对接第三方数据时,只要结构对得上,就能直接用,不必像 Java 那样为了“名义兼容”做一堆继承和实现。但缺点是,类型系统无法区分“结构相同但含义完全不同的两个东西”。比如两个接口都有 id: string,你可以把订单 id 传给用户 id 的函数而不报错。这是结构化类型的固有取舍,面试里能讲出这层 trade-off,会显得你真懂。
1.3 层次感:TS 不是“替换 JS”,而是在 JS 外面加了四层东西
我建议面试时把答案拆成三个层面:
-
语法层面:TS 在 JS 之上增加了类型语法,比如
interface、type、enum、泛型;还承担了一部分“转译”工作,把带有新语法的代码编译成目标版本的 JS。要注意的是,并不是所有 TS 特性都只是“类型”。enum、namespace、构造函数的参数属性、装饰器这些特性会生成真实运行时代码。这个区别曾经坑过很多人。 -
检查层面:JS 的类型检查发生在运行期,TS 的静态类型检查发生在编译期。正因为是编译期检查,它才能提前发现问题,并且给编辑器提供智能提示、自动补全、安全重构的基础。
-
工程层面:TS 除了类型检查,背后还有完整的模块解析、声明文件、工程配置体系。这也是为什么很多面试题表面考语法,实际考的是工程能力。
面试如果只让你“一句话说清 TS 和 JS 的区别”,你可以说:JS 的类型是运行时的、动态的、几乎不设防的;TS 在 JS 之外增加了一套编译期的静态类型系统,用来约束代码、表达契约、提前发现问题,并且绝大多数类型不会进入最终运行代码。这个回答的密度和信息量,比“TS 是 JS 的超集”高出一个档次。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 类型系统高频考点:any、unknown、never、联合类型和类型收窄
2.1 any、unknown、never、void 的区别,是很经典的送命题
这四个类型经常放在同一道题里问,因为都用“空”或“未知”来描述值,但语义完全不同。
any 是 TypeScript 里的“没有门槛”类型。把一个值标成 any,等于对这个值关闭了所有类型检查。它可以赋给任何类型,也可以接收任何类型。很多新手遇到报错就 as any,写多了代码里全是坑。
unknown 是类型安全的 any。它也能接收任意类型的值,但在收窄之前,你不能直接把它当别的类型用。这个限制是故意的——它逼着你先做校验,再去使用数据。
ts复制let data: unknown;
data = JSON.parse(response); // ok
data.name; // 报错:data 类型为 unknown
想用这个值,得先走类型守卫或收窄:
ts复制if (
typeof data === "object" &&
data !== null &&
"name" in data &&
typeof (data as Record<string, unknown>).name === "string"
) {
console.log((data as { name: string }).name);
}
never 表示“永远不可能出现的类型”。函数抛异常、无限循环、switch 里穷尽所有 case 后的默认分支,都会和 never 打交道。
ts复制function throwError(msg: string): never {
throw new Error(msg);
}
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number };
function area(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.side ** 2;
default:
const _exhaustiveCheck: never = shape; // 如果未来加了新类型且忘了处理,这里会报错
return _exhaustiveCheck;
}
}
void 表示函数的返回值不存在。注意,如果函数显式返回 undefined,类型也可以标注为 void,日常写 const fn = (): void => {} 是比较常见的。
面试时如果问到什么类型可以替换“我不在乎类型”的场景,正确的回答顺序应该是:先想想能不能用 unknown,再考虑收窄,实在不行才用 any。any 是逃生舱,不是弹药库。
2.2 类型收窄和类型守卫,是 TS 业务开发里出现频率最高的能力
类型收窄的意思是:一个值的静态类型范围较大(比如 string | number),通过条件判断把它在某个分支内变成更具体的类型。常见的收窄手段包括:
typeof:处理原始类型。instanceof:处理 class 实例。Array.isArray:判断数组。in:判断对象里是否存在某个属性。- 可辨识联合的属性值判断:比如
switch (shape.kind)。 - 自定义类型谓词
value is T:自己写守卫函数。
手写类型守卫是面试题重灾区。比如要求把一个从接口拿到的 unknown 校验成 CartItem,考察点往往是“你知不知道在类型层面校验的同时,还要在运行时真正做检查”。
ts复制interface CartItem {
id: string;
price: number;
count: number;
}
function isCartItem(value: unknown): value is CartItem {
if (typeof value !== "object" || value === null) return false;
const record = value as Record<string, unknown>;
return (
typeof record.id === "string" &&
typeof record.price === "number" &&
typeof record.count === "number"
);
}
const raw = JSON.parse(storageContent);
if (isCartItem(raw)) {
console.log(raw.price * raw.count);
}
value is CartItem 这种谓词语法是难点,它表达的意思是:当函数返回 true 时,编译器就知道传入的值已经是 CartItem,后续可以放心访问属性。
2.3 字面量类型、联合类型和“as const”陷阱
TS 里可以把类型精确到一个具体的值:
ts复制type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
let method: HttpMethod = "GET";
method = "FETCH"; // 报错
字面量类型最难受的地方,是对象或数组经过推导后会被“放宽”成基础类型。很多面试题会考察这个现象:
ts复制const config = {
protocol: "https",
method: "GET",
};
function request(method: "GET" | "POST") {}
request(config.method); // 报错:config.method 被推断成了 string
解决办法是加 as const,让 TS 知道这个对象里的字段是不允许改的字面量:
ts复制const config = {
protocol: "https",
method: "GET",
} as const;
request(config.method); // ok
另外,as const 还能把数组推导成只读元组,这也是后面讲类型体操时经常用到的基础能力。
2.4 interface 和 type,到底有什么区别
这个话题是面试里点名率最高的问题。我的建议是别背“能做什么不能做什么”的碎片列表,把区别按三个底层能力来记:
- 声明合并。
interface支持多次声明并自动合并,type不支持。这个特性让 interface 非常适合用来扩展第三方库的类型,或者说在全局声明里补充Window上的自定义属性。 - 表达能力。
type更强,能表示联合类型、交叉类型、元组、通过映射/条件创建的新类型。interface只能用来描述对象、函数、类的结构。 - 继承语法。
interface用extends,type用&交叉类型。两者在多数对象场景下效果相似。
官方比较推荐的一个倾向是:要用到联合类型、交叉类型、工具类型生成的新类型时用 type;描述传统对象结构、类接口约定时用 interface。但社区也有不少人干脆全部统一用 type,为了保持一致避免混用,这本身也没问题。关键是你在面试时要能解释清楚“我为什么在这个场景下选了这种写法”。
2.5 可辨识联合:让“非法状态”在类型层面不可表达
很多业务代码会出现大量可选字段,结果一个对象上同时存在互相矛盾的状态。比如一个异步请求的结果,字段一会儿来自 loading 状态,一会儿来自 success 状态,如果全做成可选,代码里到处都要判空。
可辨识联合的思想是:用同一个字段(通常是 kind 或 status 这种字面量类型)来区分不同分支,每个分支只放自己需要的字段,让非法组合无法通过编译:
ts复制type AsyncResult<T> =
| { status: "pending" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function handleResult<T>(result: AsyncResult<T>) {
if (result.status === "pending") {
console.log("loading");
} else if (result.status === "success") {
console.log(result.data); // 这里能直接访问 data,不用再判空
} else {
console.error(result.error);
}
}
这类代码在 reducer、状态机、表单步骤等场景里非常实用。面试时遇到“多个可选字段不如一个联合类型”的数据建模题,本质上考的就是可辨识联合的应用。
3. 泛型与类型体操:从“会用工具类型”到“能手写工具类型”
第二阶段面试通常会上代码。现在很多前端岗位的面试题里,会让候选人手写实现一个 Partial、Pick、ReturnType 或者简单的泛型函数。这部分考的不是你能不能背出源码,而是你理解不理解“泛型是在类型层面做逻辑抽象”。
3.1 泛型为什么存在:类型参数要建立“关系”
没有泛型时,你写函数只能给参数一个固定类型,或者干脆 any。但很多场景里,函数参数和返回值之间有关系:传进去什么类型,返回的就应该是同一个类型或用这个类型推导出的相关类型。泛型就是用来表达这种关系的:
ts复制function identity<T>(value: T): T {
return value;
}
const a = identity("hello"); // a: "hello"
const b = identity(42); // b: number
面试里最经典的泛型例子是“取对象属性”:
ts复制function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const cart = { price: 10, count: 2 };
getProperty(cart, "price"); // ok,返回 number
getProperty(cart, "name"); // 报错,name 不是 cart 的属性
这里有两个考点:K extends keyof T 表示 K 必须是 T 的属性名之一;T[K] 是索引访问类型,用来表示“T 的 K 属性对应的类型”。能把这个例子讲清楚,说明你对泛型的基本理解是到位的。
3.2 手写常用工具类型:核心是三种“类型操作”
很多面试者去背 Partial、Required、Pick、Omit 这些工具类型的结果,却不知道怎么实现。真正有用的思路是掌握三种基础操作:
第一种是映射类型 [K in keyof T],它遍历对象的所有属性。核心工具类型基本都是这么写出来的:
ts复制type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
第二种是按条件选择属性 K extends keyof T。Pick 的实现逻辑是“只遍历你指定的那部分属性”:
ts复制type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
interface User {
name: string;
age: number;
email: string;
}
type UserName = MyPick<User, "name" | "email">;
// { name: string; email: string }
Omit 可以用 Exclude 反过来排除掉不需要的属性,这里涉及第三个基础操作:条件类型。
ts复制type MyExclude<T, U> = T extends U ? never : T;
type A = MyExclude<"a" | "b" | "c", "a">; // "b" | "c"
条件类型里有一个容易让新手懵的细节:当 T 是裸类型参数时,条件类型会对联合类型的成员逐个判断并重新组合,这叫“分配式条件类型”。上面的 Exclude 就是基于这个特性实现的。
3.3 用 infer 提取类型:ReturnType 的高频考法
TS 的 infer 关键字用来在条件类型中“声明一个待推断的类型变量”。最典型的应用是取函数的返回值类型:
ts复制type MyReturnType<T extends (...args: any) => any> = T extends (
...args: any[]
) => infer R
? R
: never;
declare function fetchUser(id: string): Promise<{ name: string }>;
type Result = MyReturnType<typeof fetchUser>; // Promise<{ name: string }>
同理可以从函数参数中提取某一个参数的类型,从数组里提取元素类型:
ts复制type ElementOf<T extends readonly unknown[]> = T extends readonly (infer E)[]
? E
: never;
type Item = ElementOf<Array<string | number>>; // string | number
面试时如果要求手写 ReturnType,核心步骤就是三步:约束参数类型是函数、在条件类型里用 infer 推断返回值、拿到后原样返回。不理解这三步的人只能背,理解了的人可以随手写出各种变体。
3.4 递归映射的边界:写 DeepPartial 时必须想清楚的问题
进阶一点的题型是实现递归的 DeepPartial:
ts复制type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Order {
id: number;
user: {
name: string;
address: { city: string; street: string };
};
}
type PartialOrder = DeepPartial<Order>;
注意,朴素递归写法遇到 Date、Map、Set、Promise 这些内置类型时会出问题,因为 Date 的内部结构也会被递归变成 DeepPartial<Date>,导致丢失方法。更稳妥的递归类型通常会先用“是否数组 / 是否函数 / 是否基础类型 / 是否有特殊 tag”做判断。面试时能主动指出这个边界问题,绝对是个加分项,因为很多候选人只会照着模板背。我的建议是,面试前把 Partial、Pick、Omit、ReturnType、DeepPartial 这五个经典实现亲手写一遍,理解每一步的意图,就足够覆盖大多数面试场景了。
4. tsconfig 与工程配置:面试题不会直接考,但翻车率极高
TypeScript 的知识点不只在类型语法里,还藏在 tsconfig.json 的配置项中。很多候选人面试时对类型语法对答如流,一聊到项目里的编译配置就露怯。下面这几个配置相关话题,是面试官非常爱问的工程细节。
4.1 strict 开关背后到底开了什么
很多新项目里 strict: true 已经是默认配置,但真正让人说清它包含什么,很多候选人会卡壳。strict 不是单个选项,它是一系列严格检查的集合,其中最有代表性的两个是 strictNullChecks 和 noImplicitAny。
开启 strictNullChecks 后,null 和 undefined 不再能赋值给任意类型。比如:
ts复制function greet(name: string) {
return `hello ${name}`;
}
greet(null); // 开启 strict 后直接报错
这会导致代码量明显增加,因为你必须把可能为空的场景显式收窄掉。所以很多老项目升级到新 TS 版本时会一片飘红,不是编译器坏了,而是类型系统在逼你处置那些原本被忽略的空值风险。
简单理解:noImplicitAny 禁止“某个参数因为没写类型而被悄悄当成 any”,strictNullChecks 禁止“空值被悄悄当成普通值用”。把这两个讲清楚,面试官就能判断你是真的开过严格模式,而不是只会抄配置。
4.2 模块解析和相关互操作选项
面试题里经常出现的还有 esModuleInterop、allowSyntheticDefaultImports、moduleResolution 这一组。这些是 TS 的模块系统与运行环境之间的兼容桥梁。
早期用 CommonJS 的代码导出一个对象时,require("react") 得到的是整个模块对象,但在 ESM 语法里,import React from "react" 是取默认导出。React 本身并没有默认导出,那为什么现在的 TS 项目里能直接这么写?因为 esModuleInterop: true 允许 TS 在编译时做一层兼容,把 CommonJS 的导出模拟成默认导出。如果面试者能解释这个业务背景,肯定比只说“这是标准配置”强很多。
模块解析现在还有一个高频坑:使用 Vite、esbuild、webpack 这类打包器时,解析规则和平常 tsc 的 classic/node 模式不一样。TS 为了让配置跟上现代打包器,提供了 moduleResolution: "bundler" 选项。很多把 moduleResolution 默认值用到底的人,项目中一旦出现依赖 package.json 里 exports 字段限制的包,就会遇到奇怪的报错。
4.3 声明文件与类型边界
另一个工程化考点是 *.d.ts。核心要知道:.d.ts 文件只描述类型,不产生任何运行时代码,它是 JS 世界与 TS 世界之间的一座桥。
如果项目中引入了一个没有自带类型的第三方库,你可以自己补一个声明。理想情况下是声明真实存在的函数签名;偷懒的做法是直接 declare module 'xxx',把整个模块声明成 any,这样问题看似消失,实际上类型保护也跟着失效了:
ts复制declare module "some-untyped-lib";
面试时遇到“项目中某个 npm 包没有类型怎么办”这个问题,比较好的回答思路是:先去 DefinitelyTyped 找有没有对应 @types/ 包;没有就自己做模块补充声明,明确的模块声明函数签名优于整包 any;如果涉及升级维护,优先看它是否提供了官方类型文件或 exports 里的 types 字段。按这个顺序回答,面试官会认为你真的处理过这个问题。
5. 实际项目里的高频坑与类型设计经验
到这一层,已经不光是面试题了,而是“会写 TS”和“写得好 TS”的分水岭。下面这些类型坑,我在 code review 和修 bug 时反复见到。
5.1 TS 类型不能代替运行时校验,接口数据要过一层“真相确认”
这是前端和 Node 工程里最重要的认知。TS 的类型系统只在你“编译期可见的代码”范围内生效,而接口返回的数据是运行时才到达的外部输入。编译器无法知道后端到底返回了什么。
这就出现了一个尴尬现象:你给 fetch 写好了 Promise<CartItem[]>,如果后端某天把 price 从数字改成了字符串,线上代码不会因为你写了类型而自动报错,等用户打开页面才发现金额展示异常。解决思路是引入运行时校验,同时让校验器的 schema 推导出 TS 类型,让“运行时真相”和“编译期类型”保持同源:
ts复制import { z } from "zod";
const CartItemSchema = z.object({
id: z.string(),
price: z.number(),
count: z.number(),
});
type CartItem = z.infer<typeof CartItemSchema>;
function parseCartItems(value: unknown): CartItem[] {
return z.array(CartItemSchema).parse(value);
}
这种模式的好处是:校验规则是唯一事实源,类型由它推导而来,不会出现“校验器写一套、interface 又写一套”导致的漂移。不同校验库之间现在也在尝试统一接口描述规范,比如 standard schema 这类通用契约,让库和库之间可以互相协作。面试时提到这个趋势,比单纯说“我会用 zod”更有信息量。
5.2 Object.keys 返回 string[] 的原因与处理方式
一个在业务代码里非常经典的报错是这样的:
ts复制const config: Record<string, string> = {
name: "Alice",
city: "Shanghai",
};
Object.keys(config).forEach((key) => {
console.log(config[key]); // 报错:key 是 string,不能用来索引 config
});
原因是 TS 选择了比较保守的类型推断:对象可能存在额外属性,Object.keys 返回的 key 并不保证一定是原来声明的那几个属性。这在大多数场景下是对的类型约束。真正的处理方案不是用 key as keyof typeof config 强行断言,而是先想清楚你要遍历的对象是否真的是封闭的。如果确实是内部常量对象,可以这样处理:
ts复制const config = {
name: "Alice",
city: "Shanghai",
} as const satisfies Record<string, string>;
Object.keys(config).forEach((key) => {
console.log(config[key as keyof typeof config]);
});
我见过很多人一遇到这个报错就 as any,问题看似消失,后续整个对象的类型保护也一起消失了。正确姿势是理解 TS 为什么这样设计,再做最小范围的断言。
5.3 用“非法状态不可表达”的思路设计类型,而不是堆可选字段
有经验的 TS 开发者会刻意让类型只允许合法组合。拿订单状态举例,如果写成:
ts复制interface Order {
status: "pending" | "paid" | "shipped";
paidAt?: Date;
shippedAt?: Date;
}
那你就要无条件处理 paidAt、shippedAt 可能为 undefined 的场景,而且无法阻止有人写出 { status: "pending", shippedAt: new Date() } 这种自相矛盾的数据。更稳的建模是拆成可辨识联合:
ts复制type Order =
| { status: "pending" }
| { status: "paid"; paidAt: Date }
| { status: "shipped"; paidAt: Date; shippedAt: Date };
这种写法的价值在于:非法状态在编译期就是不可能的。团队协作时,只要类型定义清楚了,其他人想写出错误数据都无从下手。这个设计思路比任何单个语法点都更能体现你的类型功力。
5.4 慎用 enum,字符串字面量联合在很多场景更合适
TS 的 enum 会生成真实运行时代码,并且数字枚举还支持反向映射,这在不少团队里成了争议点。如果需要的只是几个固定的字符串常量,强烈建议用 as const 加联合类型替代:
ts复制const Status = {
Pending: "pending",
Paid: "paid",
Shipped: "shipped",
} as const;
type Status = (typeof Status)[keyof typeof Status];
这样可以同时拿到“用于运行时的常量对象”和“用于类型检查的联合类型”,而且代码体积比 enum 更小,语义也更直观。面试时能根据场景说出“我不用 enum 的考虑”,比熟背 enum 使用语法更能获得认可。
6. 从面试官视角看:面试时想听到什么,以及怎么准备
我参与面试这些年,一个很深的感受是:很多候选人把大量精力花在反复刷类型体操题上,结果真正问到一个贴合实际的业务场景,反而答不出如何建模。面试官其实会分层次观察候选人的 TS 能力。
6.1 第一层:能否讲清原理,而不是背结论
面试官先会确认你是否理解 TS 存在的意义。这里考察的不是记忆,而是判断力。能说出“TS 把类型检查提前到编译期,便于在 IDE 中即时反馈”和能说出“类型系统还会影响代码结构设计,帮助我们约束非法状态”是完全两回事。前者是会用,后者是想通了。
6.2 第二层:能否在真实场景里做出合理的取舍
有经验的面试官不会只让候选人做手写题,他会找一个贴近业务的需求。比如:“让你设计一个前端配置系统,配置项会来自后端,需要支持字符串、数字、布尔值和嵌套对象,你会怎么定义类型和校验流程?”这种题目没有标准答案,考的是你是否会在类型灵活性和安全性之间做权衡,是否知道用可辨识联合,是否会在边界上补运行时校验。
6.3 第三层:能否给团队留下可维护的类型资产
这一点通常不在面试题里直接出现,但会在项目和 code review 中体现。类型定义也是一种代码资产,写得好的类型可以让新成员快速理解数据结构,写差的类型只会让代码库更混乱。面试者如果在项目经历里能讲出“我曾经把一个全是可选字段的接口拆成了可辨识联合,减少了几十处判空”,面试官对候选人的评价会明显上升。
6.4 学习路线上的一点建议
如果目标是应对 TypeScript 面试同时还要能落地,我的个人经验是:
- 先把
strict模式下的基础类型、联合类型、可辨识联合用熟,这是日常写业务的核心。 - 再掌握常用工具类型的使用场景,目标不是背源码,而是看到
Omit<User, "password">能立刻反应出“生成一个去掉某个字段的新类型”。 - 泛型和类型体操可以适当练习,但要根据目标岗位控制深度。非基础架构岗位不要求你把各种高阶体操都玩出花,能解释并写出常用工具类型基本就够。
- 架构类、基础库类岗位,备考侧重点要明显偏向泛型、条件类型、infer、模块声明和工具类型实现,并需要补充对多种运行时环境和模块解析机制的理解。
准备面试时,最有效率的方式是拿自己参与过的真实项目做复盘:你项目里哪些类型设计让你后来少踩了很多坑?哪些类型写错了导致线上出过问题?这些真实案例,比一百道背诵题都有说服力。
最后再说一点我的实际体会。面试中能答出漂亮“标准答案”的人不少,但真正让面试官眼前一亮的人,往往是在讲题之外流露出的那种“类型思维”——遇到新数据先想它可能有什么形态,写函数先想参数和返回值之间有什么关系,看代码时习惯于通过类型推断调用方是不是用错了。这种思维方式不会因为突击背题而来,它是在日复一日的真实代码里长出来的。所以如果你还来得及,别把 TS 面试准备当成一门“背题冲刺课”,把它当成一次重新审视自己代码质量的机会。
