3785 字
19 分钟
TypeScript 进阶面试题(中高级必考)
NOTE本文包含 20+ 道 TypeScript 进阶面试题,中高级岗位 TS 考察比重越来越高,难度标注:⭐ 基础 · ⭐⭐ 进阶 · ⭐⭐⭐ 高级 · 🔥 高频必考
类型基础
Q1: interface 和 type 的区别?⭐ 🔥
答:
| 维度 | interface | type |
|---|---|---|
| 声明合并 | ✅ 同名自动合并 | ❌ 重复定义报错 |
| extends / 交叉 | extends 继承 | & 交叉类型 |
| 联合/元组/基础类型别名 | ❌ | ✅ |
| 映射类型 | ❌ | ✅ |
| 性能 | 类型检查更快(名字可缓存) | 复杂计算类型稍慢 |
| 报错信息 | 更友好(显示接口名) | 有时展开为内联结构 |
// interface 声明合并(适合扩展第三方库类型)interface User { name: string }interface User { age: number }// 合并为 { name: string; age: number }
// type 独有:联合、元组、条件类型type Status = 'pending' | 'done'; // 联合type Pair = [string, number]; // 元组type NonNull<T> = T extends null ? never : T; // 条件类型
// 实践建议:// 对象/类的形状 → interface(可合并、可 implements)// 联合、元组、工具类型、类型运算 → typeQ2: any、unknown、never 的区别?⭐ 🔥
// any:放弃类型检查,可以赋值给任何类型、做任何操作let a: any = 'str';a.foo.bar(); // 编译通过,运行时爆炸
// unknown:类型安全的 any,赋值随意,使用前必须收窄let u: unknown = JSON.parse(str);u.toFixed(); // ❌ 编译报错if (typeof u === 'number') u.toFixed(); // ✅ 收窄后可用
// never:不可能存在的值,是所有类型的子类型// 三大来源:// 1. 抛异常的函数function fail(msg: string): never { throw new Error(msg); }// 2. 无限循环function loop(): never { while (true) {} }// 3. 穷尽检查(最高频用法)type Shape = 'circle' | 'square';function area(s: Shape) { switch (s) { case 'circle': return 1; case 'square': return 2; default: // 如果 Shape 新增了 'triangle' 而这里没处理, // s 会是 'triangle',无法赋给 never → 编译报错,提醒补全 const _exhaustive: never = s; return _exhaustive; }}Q3: 什么是类型收窄(Narrowing)?有哪些方式?⭐⭐ 🔥
// 1. typeof —— 基础类型function f(x: string | number) { if (typeof x === 'string') x.toUpperCase(); // x 收窄为 string}
// 2. instanceof —— 类实例if (err instanceof Error) console.log(err.message);
// 3. in —— 属性存在性interface Dog { bark(): void }interface Cat { meow(): void }function speak(pet: Dog | Cat) { if ('bark' in pet) pet.bark();}
// 4. 字面量类型判别(可辨识联合,最常用)type Res = { code: 0; data: string } | { code: -1; msg: string };function handle(r: Res) { if (r.code === 0) r.data; // 通过判别属性收窄 else r.msg;}
// 5. 自定义类型守卫(is 谓词)function isString(v: unknown): v is string { return typeof v === 'string';}
// 6. 断言函数(asserts)function assertDefined<T>(v: T | undefined): asserts v is T { if (v === undefined) throw new Error('undefined');}
// 7. 真值收窄 / == null / 可选链if (user?.name) { /* user 非空 */ }泛型
Q4: 泛型是什么?解决什么问题?⭐ 🔥
// 没有泛型:要么丢失类型(any),要么为每种类型写一遍function identity(arg: any): any { return arg; } // 返回值类型丢失
// 泛型:类型参数化,调用时才确定类型function identity<T>(arg: T): T { return arg; }const s = identity('hello'); // T 推断为 string,s: stringconst n = identity<number>(1); // 显式指定
// 泛型接口 / 泛型类interface ApiResponse<T> { code: number; data: T; message: string;}type UserRes = ApiResponse<{ id: number; name: string }>;
// 泛型约束(extends):限制 T 必须具备某些属性function getLength<T extends { length: number }>(arg: T): number { return arg.length;}getLength('str'); // ✅ string 有 lengthgetLength([1, 2]); // ✅ 数组有 lengthgetLength(123); // ❌ number 没有 length
// 默认泛型参数function createArr<T = string>(len: number, val: T): T[] { return Array(len).fill(val);}Q5: 泛型的 extends 有哪几种含义?⭐⭐
// 1. 约束:T 必须满足某形状function fn<T extends { id: number }>(arg: T) {}
// 2. 继承接口interface Admin extends User { role: string }
// 3. 条件类型中的判断(类型层面的三元表达式)type IsString<T> = T extends string ? true : false;type A = IsString<'x'>; // truetype B = IsString<1>; // false条件类型与 infer
Q6: 条件类型的分布式特性?⭐⭐⭐ 🔥
// 当条件类型作用于联合类型时,会分发到每个成员分别计算type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>;// 分布式:ToArray<string> | ToArray<number> = string[] | number[]// 而不是 (string | number)[]
// 如何关闭分布式?用元组包一层type ToArrayNoDist<T> = [T] extends [any] ? T[] : never;type R2 = ToArrayNoDist<string | number>; // (string | number)[]
// 经典应用:Exclude 就是靠分布式实现的type MyExclude<T, U> = T extends U ? never : T;type E = MyExclude<'a' | 'b' | 'c', 'a'>;// 'a' extends 'a' → never// 'b' extends 'a' → 'b'// 'c' extends 'a' → 'c'// never | 'b' | 'c' = 'b' | 'c'(never 在联合中消失)Q7: infer 关键字的作用?⭐⭐⭐ 🔥
// infer:在条件类型中声明一个待推断的类型变量// 相当于类型层面的"解构赋值"
// 提取函数返回值类型(ReturnType 的实现)type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;type R1 = MyReturnType<() => string>; // string
// 提取 Promise 的值类型type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;type P = UnwrapPromise<Promise<Promise<number>>>; // number(递归解包)
// 提取数组元素类型type ElementOf<T> = T extends (infer E)[] ? E : never;type E = ElementOf<string[]>; // string
// 提取第一个元素type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;type F = First<[1, 2, 3]>; // 1
// 提取构造函数实例类型(InstanceType 的实现)type MyInstance<T> = T extends new (...args: any[]) => infer I ? I : never;映射类型与 keyof
Q8: keyof 和 typeof 的用法?⭐ 🔥
// keyof:取对象类型的所有键,组成联合类型interface User { id: number; name: string; age: number }type UserKey = keyof User; // 'id' | 'name' | 'age'
// 高频场景:类型安全的属性访问function getProp<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key];}const user: User = { id: 1, name: 'Tom', age: 20 };getProp(user, 'name'); // 返回类型精确推断为 stringgetProp(user, 'xxx'); // ❌ 编译报错,key 不在 User 中
// typeof:从"值"反推"类型"(JS 对象 → TS 类型)const config = { host: 'localhost', port: 8080 } as const;type Config = typeof config; // { readonly host: "localhost"; readonly port: 8080 }type ConfigKey = keyof typeof config; // 'host' | 'port'
// 组合技:常量数组 → 联合类型const ROLES = ['admin', 'user', 'guest'] as const;type Role = typeof ROLES[number]; // 'admin' | 'user' | 'guest'Q9: 映射类型是什么?修饰符怎么用?⭐⭐⭐
// 映射类型:遍历键集合批量生成新类型type MyPartial<T> = { [K in keyof T]?: T[K]; // in 遍历,? 添加可选修饰};
// 修饰符的添加与移除:+ / -type Mutable<T> = { -readonly [K in keyof T]: T[K] }; // 移除 readonlytype Freeze<T> = { +readonly [K in keyof T]: T[K] }; // 添加 readonlytype Required2<T> = { [K in keyof T]-?: T[K] }; // 移除可选
// as 子句(TS 4.1+):重映射键// 1. 过滤键type OnlyString<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K];};// 2. 生成 getter 类型type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];};interface Person { name: string; age: number }type PersonGetters = Getters<Person>;// { getName: () => string; getAge: () => number }Q10: 手写常见内置工具类型?⭐⭐⭐ 🔥
// Partial:所有属性变可选type MyPartial<T> = { [K in keyof T]?: T[K] };
// Required:所有属性变必选type MyRequired<T> = { [K in keyof T]-?: T[K] };
// Readonlytype MyReadonly<T> = { readonly [K in keyof T]: T[K] };
// Pick:挑选属性type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
// Omit:排除属性(Pick + Exclude 组合)type MyOmit<T, K extends keyof T> = MyPick<T, MyExclude<keyof T, K>>;// 官方实现:Pick<T, Exclude<keyof T, K>>
// Exclude / Extracttype MyExclude<T, U> = T extends U ? never : T;type MyExtract<T, U> = T extends U ? T : never;
// NonNullable:排除 null 和 undefinedtype MyNonNullable<T> = T extends null | undefined ? never : T;
// Record:构造键值对类型type MyRecord<K extends keyof any, V> = { [P in K]: V };type Dict = MyRecord<string, number>; // { [x: string]: number }
// ReturnType / Parameterstype MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;type MyParameters<T> = T extends (...args: infer P) => any ? P : never;实战场景
Q11: 如何给后端接口返回值做类型定义?⭐⭐
// 1. 统一响应包装(泛型)interface ApiResult<T> { code: number; message: string; data: T;}
// 2. 列表分页响应interface PageResult<T> { list: T[]; total: number; pageNum: number; pageSize: number;}
// 3. 实体类型集中管理interface UserVO { id: number; username: string; status: UserStatus; createTime: string;}
// 枚举用字面量联合 + as const(比 enum 更轻量,tree-shaking 友好)const UserStatus = { NORMAL: '0', DISABLED: '1' } as const;type UserStatus = typeof UserStatus[keyof typeof UserStatus];
// 4. 请求函数泛型化,全链路类型推断async function request<T>(url: string, config?: RequestInit): Promise<ApiResult<T>> { const res = await fetch(url, config); return res.json();}// 调用处 data 自动推断为 PageResult<UserVO>const { data } = await request<PageResult<UserVO>>('/api/users');data.list.forEach(u => u.username); // ✅ 全程有提示Q12: declare、.d.ts、模块声明怎么用?⭐⭐
// declare:告诉 TS "这个东西在别处已存在",只声明不实现declare const __VERSION__: string; // 构建工具注入的全局变量declare function ga(cmd: string): void; // 第三方脚本挂载的全局函数
// .d.ts 文件:纯类型声明,不产生运行时代码// global.d.ts —— 扩展全局类型interface Window { __APP_CONFIG__: { apiBase: string };}// 之后 window.__APP_CONFIG__ 有类型提示
// 为无类型的 npm 包补声明declare module 'some-untyped-lib' { export function doSomething(input: string): number;}
// 静态资源模块声明(Vite 项目常见)declare module '*.svg' { const content: string; export default content;}
// 扩展第三方库类型(模块扩充)// 例如给 vue-router 的 meta 加类型declare module 'vue-router' { interface RouteMeta { requiresAuth?: boolean; roles?: string[]; }}Q13: as const 和 satisfies 的作用?⭐⭐⭐
// as const:深度只读 + 字面量类型收窄const arr1 = ['a', 'b']; // string[]const arr2 = ['a', 'b'] as const; // readonly ['a', 'b']const obj = { type: 'click' } as const; // { readonly type: "click" }// 没有 as const 时 type 会被 widen 成 string
// satisfies(TS 4.9+):既要满足约束,又保留精确类型type ColorMap = Record<string, string | [number, number, number]>;
const colors = { red: [255, 0, 0], green: '#00ff00',} satisfies ColorMap;// colors.red 类型是 [number, number, number](精确保留)colors.red[0]; // ✅ 能访问元组下标// 如果写成 : ColorMap,red 会被放宽为 string | [number,number,number],// 访问 red[0] 报错
// 总结:// as const → 收窄为字面量/只读// satisfies → 校验类型但不丢失精度(常与 as const 连用)Q14: 协变与逆变是什么?⭐⭐⭐
// 协变(covariance):子类型可以赋给父类型(数组、返回值)class Animal { name = '' }class Dog extends Animal { bark() {} }
let animals: Animal[] = [];let dogs: Dog[] = [];animals = dogs; // ✅ Dog[] 赋给 Animal[](协变)
// 逆变(contravariance):函数参数方向相反type Handler<T> = (arg: T) => void;let hAnimal: Handler<Animal> = a => console.log(a.name);let hDog: Handler<Dog> = d => d.bark();hDog = hAnimal; // ✅ 处理 Animal 的函数可以处理 Dog(逆变)// 直觉:能处理"更宽"参数的函数,用在"更窄"参数位置是安全的
// 双向协变(bivariance):方法写法的参数默认双向协变(不严格)// strictFunctionTypes 开启后,函数写法的参数才是严格逆变
// 面试一句话总结:// 属性/返回值 → 协变;函数参数 → 逆变(strict 模式下)Q15: TS 编译过程?tsc 做了哪些事?⭐⭐
TypeScript 编译流程:
.ts 源码 ↓ 解析(Parser)AST 抽象语法树 ↓ 绑定(Binder)→ Symbol 符号表 ↓ 类型检查(Checker)← 核心!tsc 的主要价值 ↓ 发射(Emitter).js + .d.ts + .map
关键点:1. tsc = 类型检查 + 转译降级(target 转换)2. Vite/esbuild/SWC 只转译不检查(快 20-100 倍), 类型检查交给 IDE 和 CI 里的 vue-tsc / tsc --noEmit3. isolatedModules:每个文件独立编译, 因此不能用 const enum、命名空间合并等跨文件特性4. 类型信息在编译后完全擦除(type erasure), 运行时不存在任何类型,无法用类型做运行时校验 → 外部数据(接口返回)需要 zod / valibot 等运行时校验Q16: tsconfig 中最重要的配置项?⭐⭐ 🔥
{ "compilerOptions": { // 严格性(新项目直接开 strict,包含下面所有) "strict": true, "noImplicitAny": true, // 禁止隐式 any "strictNullChecks": true, // null/undefined 必须显式处理 "strictFunctionTypes": true, // 函数参数逆变检查
// 模块与目标 "target": "ES2020", // 编译产物的 JS 版本 "module": "ESNext", // 模块系统 "moduleResolution": "bundler",// Vite/webpack 项目推荐
// 互操作 "esModuleInterop": true, // 允许 import x from 'cjs-module' "allowSyntheticDefaultImports": true, "resolveJsonModule": true, // 允许 import json
// 路径别名(与 Vite resolve.alias 保持一致) "baseUrl": ".", "paths": { "@/*": ["src/*"] },
// 产物 "sourceMap": true, "declaration": true, // 生成 .d.ts(发 npm 包必须) "skipLibCheck": true // 跳过 node_modules 类型检查(提速) }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"]}类型体操常见题
Q17: 手写常见类型体操题?⭐⭐⭐ 🔥
// 1. 元组转联合type TupleToUnion<T extends any[]> = T[number];type U = TupleToUnion<['a', 'b', 'c']>; // 'a' | 'b' | 'c'
// 2. 联合转交叉(高频难题)type UnionToIntersection<T> = (T extends any ? (arg: T) => void : never) extends (arg: infer I) => void ? I : never;// 原理:联合分发成多个函数类型,再利用函数参数逆变推断出交叉type I = UnionToIntersection<{ a: 1 } | { b: 2 }>; // { a: 1 } & { b: 2 }
// 3. 字符串模板解析(CamelCase → KebabCase)type CamelToKebab<S extends string> = S extends `${infer H}${infer T}` ? H extends Uppercase<H> ? `${H extends Lowercase<H> ? '' : '-'}${Lowercase<H>}${CamelToKebab<T>}` : `${H}${CamelToKebab<T>}` : S;type K = CamelToKebab<'helloWorldFoo'>; // 'hello-world-foo'
// 4. Awaited(TS 4.5 内置,递归解包 Promise)type A = Awaited<Promise<Promise<string>>>; // string
// 5. 数组转对象(元组 [keys, values] 映射)type Zip<K extends string[], V extends any[]> = K extends [infer KH extends string, ...infer KR] ? V extends [infer VH, ...infer VR] ? { [P in KH]: VH } & Zip<KR, VR> : {} : {};
// 6. DeepReadonly(递归只读)type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];};
// 7. DeepPartial(递归可选)type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];};
// 学习建议:type-challenges 仓库刷 easy 全部 + medium 前 30 题,// 面试手写基本覆盖(联合转交叉、DeepReadonly、字符串模板是重点)Q18: 项目中如何做 TS 渐进式迁移(JS → TS)?⭐⭐
大型 JS 项目迁移 TS 的务实策略:
1. 先搭架子:allowJs: true,让 .js/.ts 共存2. 从底向上:类型声明(api/types)→ 工具函数 → 组件 → 页面3. 分级严格:先关 strict,跑通构建,再逐目录开启4. 双工具链:Vite 用 esbuild 转译(不检查), CI 加 tsc --noEmit 兜底全量检查5. 存量代码:@ts-expect-error + TODO 注释标记, 禁止新增 @ts-ignore(它会永久吞掉错误)6. 第三方无类型包:先 declare module 兜底, 再逐步补全或换有类型的替代包
关键指标:any 占比。用 eslint 规则@typescript-eslint/no-explicit-any 设为 warn 统计,新代码 error、旧代码 warn,逐步收敛。复习卡片
TIP快速复习清单
- interface vs type:声明合并 vs 联合/元组/映射
- any vs unknown vs never:不安全 / 安全收窄 / 穷尽检查
- 类型收窄七法:typeof、instanceof、in、判别联合、is 守卫、asserts、真值
- 分布式条件类型:联合类型逐个计算,
[T] extends [U]可关闭- infer:类型层面的解构,ReturnType/UnwrapPromise 核心
- keyof + 泛型约束:
<T, K extends keyof T>类型安全取属性- 内置工具类型手写:Partial/Pick/Omit/Exclude/Record 必须秒写
- as const vs satisfies:收窄字面量 vs 校验不丢精度
- 协变逆变:返回值协变、参数逆变(strictFunctionTypes)
- 编译流程:tsc 检查+发射;Vite 只转译;类型运行时擦除
- 高频体操题:UnionToIntersection、DeepReadonly、CamelToKebab
- 渐进迁移:allowJs 共存 → 自底向上 → any 占比收敛
TIP下一篇:性能优化面试题 涵盖核心指标(LCP/INP/CLS)、加载性能、渲染性能、内存泄漏排查、优化实战案例。
TypeScript 进阶面试题(中高级必考)
http://117.72.32.87/blog/posts/interview-guide-12-typescript/