TypeScript面试题精选

2026年 阅读约 10 分钟 面试指南 · 前端面试 · TypeScript

TypeScript面试题精选:interface与type区别、any与unknown、泛型与条件类型、infer类型推断与工具类型实现原理、strict工程化与JS渐进迁移实践,由浅入深系统梳理高频考点,附代码示例与面试问答。

一句话总结

TypeScript = JavaScript + 静态类型系统。它在源码中增加类型标注,由编译器在编译期做类型检查、生成智能提示,编译(或转译)后输出纯 JavaScript——类型只存在于编译期,运行时被完全擦除,所以 TS 没有额外运行时开销。它的核心价值不是"能发现所有 bug",而是:给大型项目提供接口契约、让 IDE 重构有安全保障、让"某个值到底是什么类型"从口头约定变成编译器可验证的事实。

# TS 的三个关键事实(面试常作开场追问): # 1. 类型擦除:interface/type/泛型标注编译后全部消失,不存在"运行时的 TS 类型" # 2. 结构化类型(鸭子类型):类型兼容看结构不看名字,class 没有私有字段时"形状相同即兼容" # 3. 渐进式:可以和 JS 混用(allowJs),从 any 起步逐步收紧到 strict,不是全有或全无

初级理解

interface 与 type 的区别(最高频)

能力interfacetype
描述对象结构可以可以
声明合并(同名自动合并)可以不可以
被 class implements可以可以(对象类型部分)
extends 继承可以(多接口继承)可以(交叉类型 &)
联合/交叉/元组/映射/条件类型不可以可以
// interface:可扩展、可合并——适合描述"对象的形状"、对外契约 interface User { id: number; name: string; } interface User { email?: string; // 声明合并:另一个文件/位置可继续扩展同一 interface } // type:表达力更强——联合类型、 mapped type、条件类型只有 type 能写 type ID = number | string; // 联合类型 type PartialUser = Partial<User>; // 映射类型 type NonNull<T> = T extends null | undefined ? never : T; // 条件类型 // 面试口径:能用 interface 就用 interface(可扩展性好、报错信息友好), // 需要联合/元组/运算类型时用 type。两者在"对象结构"上大多数场景可互换。

any 与 unknown 的区别

// any:逃生舱——对 any 的任何操作都通过检查,等于关掉类型检查 let a: any = 'hello'; a.foo.bar.baz; // 不报错,运行时炸了才知道 // unknown:类型安全的 any——赋值什么都行,但使用前必须"收窄" let u: unknown = 'hello'; u.toUpperCase(); // ✗ 报错:unknown 不能直接调用 if (typeof u === 'string') { u.toUpperCase(); // ✓ 收窄后才能用 } // 结论:unknown 比 any 安全——它强制你先证明值的类型再使用

函数类型与泛型基础

// 函数类型:参数必写类型(可被上下文推断时可省),返回值一般可推断 const add = (a: number, b: number): number => a + b; // 泛型:类型的参数化——同一个函数/类型适配多种类型,同时保留具体类型信息 function first<T>(arr: T[]): T | undefined { return arr[0]; } const n = first([1, 2, 3]); // 推断 T = number,返回 number | undefined const s = first(['a', 'b']); // T = string ——比返回 any 强太多

中级深入

类型推断是怎么工作的

# 1. 返回值推断:没有标注返回类型时,编译器从 return 语句推断联合类型 # 注意坑:if (x) return 1; return undefined; 会推断出 number | undefined,显式标注更稳 # 2. 上下文类型推断:把函数赋给有类型的变量/作为参数传入时,参数类型从上下文"倒推" const on: (e: MouseEvent) => void = (e) => e.clientX; // e 自动是 MouseEvent # 3. best common type:const x = cond ? 1 : 'a' 推断为 1 | 'a'(字面量)或 number | string # as const 把一切收窄为字面量类型,常用于配置对象/路由表

泛型约束与 keyof T[K]

// extends 约束:只有具备某些能力的类型才能传入 function get<T extends { id: string }, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; // 返回类型精确到 T[K],不是 any } get({ id: 'x', age: 1 }, 'age'); // 返回 number // 面试追问:keyof typeof 的组合拳 const route = { home: '/', user: '/user' } as const; type RouteKey = keyof typeof route; // 'home' | 'user' type RoutePath = typeof route[RouteKey]; // '/' | '/user'

条件类型与 infer:工具类型的实现原理

// 条件类型:类型层面的三元表达式(模式匹配) type IsString<T> = T extends string ? true : false; // infer:在模式匹配中"捕获"类型片段——面试要求手写的高频点 type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never; type MyParameters<F> = F extends (...args: infer P) => any ? P : never; // 分发性:裸类型参数 + 联合类型会逐个分发 type ToArray<T> = T extends any ? T[] : never; type R1 = ToArray<string | number>; // string[] | number[](不是 (string|number)[]) // 常用工具类型手写(面试常要求写出 2-3 个) type MyPartial<T> = { [K in keyof T]?: T[K] }; type MyReadonly<T> = { readonly [K in keyof T]: T[K] }; type MyPick<T, K extends keyof T> = { [P in K]: T[P] }; type MyOmit<T, K extends keyof any> = MyPick<T, Exclude<keyof T, K>>; type MyRecord<K extends keyof any, V> = { [P in K]: V }; type MyExclude<T, U> = T extends U ? never : T; // 'a'|'b' 排除 'a' → 'b'

高级进阶

函数参数的逆变与协变

# TS 类型兼容的方向性(strictFunctionTypes 开启后): # 协变:返回类型——子类型返回值可以赋给父类型返回位置(Dog 可当 Animal 用) # 逆变:参数类型——接受 Animal 的函数可以赋给接受 Dog 的位置(方向反过来),更安全 interface Animal { name: string } interface Dog extends Animal { bark(): void } // 方法语法 vs 函数属性语法,检查规则不同(经典考点): interface A1 { eat(d: Dog): void } // 方法语法:参数双变(宽松,历史上兼容) interface A2 { eat: (d: Dog) => void } // 函数属性语法:参数逆变(strict 下更严格) declare let f1: (d: Dog) => void; declare let a1: A1; declare let a2: A2; a1.eat = f1; // ✓ 方法语法允许 a2.eat = f1; // ✗ strictFunctionTypes 下报错(可能被传入非 Dog)

声明合并与类型空间

// interface 同名自动合并(扩展第三方库类型的标准手段:declare module) // type/class 同名则报错——class 在类型空间是类型、在值空间是值(双空间是 TS 核心概念) // declare global:在模块文件里扩展全局类型 declare global { interface Window { __APP_CONFIG__: { apiBase: string } } } // import type:类型专用导入,编译后整行擦除——避免循环依赖、保证纯类型不产生运行时代码 import type { Config } from './config'; // 编译后此行消失 import { loadConfig } from './config'; // 这行才会保留

结构化类型的陷阱与"名义类型"模拟

// TS 默认结构化兼容:两个 class 字段形状相同就能互赋(没有私有成员时) class Cat { name = '' } class Dog { name = '' } const c: Cat = new Dog(); // ✓(结构相同)——不想要就用 private/brand // branding:用 never 品牌制造"名义类型"效果,防止 ID 混用 type UserId = string & { readonly __brand: 'UserId' }; type OrderId = string & { readonly __brand: 'OrderId' }; const uid = 'u1' as UserId; const oid = 'o1' as OrderId; // func(uid, oid) —— ✗ 编译期就拦住"两个都是 string 所以能混传"的经典事故

实战场景

JS 项目渐进迁移到 TS 的路线

# 1. 起步:npm i -D typescript @types/node;tsconfig 开 allowJs + checkJs, # JS 文件里用 // @ts-check + JSDoc 注释就能享受类型检查,零文件改动 # 2. 新文件一律 .ts/.tsx;旧文件按模块价值排序逐个改后缀(改一个带动一片类型推导) # 3. any 策略分级:禁止新增显式 any(noImplicitAny),历史遗留用 // eslint-disable 点状豁免 # 4. 边界收口:fetch/axios 封装、localStorage 读写、JSON.parse 这些"类型黑洞"集中治理 # 5. 最后开 strict 顶级开关,清完存量告警

tsconfig 关键配置项

{ "compilerOptions": { "strict": true, // 一站式:noImplicitAny/strictNullChecks 等全家桶 "noUncheckedIndexedAccess": true, // arr[i] 是 T | undefined,防越界(可选,激进) "esModuleInterop": true, // import React from 'react' 兼容 CJS 默认导出 "moduleResolution": "bundler", // Vite 项目推荐;老项目 node "paths": { "@/*": ["src/*"] }, // 路径别名(只改类型解析,打包别名要配 Vite) "skipLibCheck": true // 跳过 .d.ts 内部检查,大幅提速 } }

axios 封装:让响应数据类型化

interface ApiResult<T> { code: number; message: string; data: T } async function request<T>(url: string, init?: RequestInit): Promise<T> { const res = await fetch(url, init); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = (await res.json()) as ApiResult<T>; if (json.code !== 0) throw new Error(json.message); return json.data; // 调用方拿到的就是精确的 T,不再是 any } // 使用:接口契约直达调用点 const user = await request<{ id: string; name: string }>('/api/user/1');
注意:类型断言(as)不是"转换",只是告诉编译器"闭嘴,我比你懂"。as any 会把类型检查链路整条打断,优先用 unknown + 收窄、泛型、类型守卫替代。

面试模拟

面试官:interface 和 type 有什么区别?什么时候必须用哪个?

你:两者都能描述对象结构,大多数场景可互换。interface 的独有能力是声明合并和被 class implements,适合描述对外契约、需要后续扩展的形状;type 的表达力更强,支持联合类型、元组、映射类型、条件类型——比如 number | string 这种联合只有 type 能写。实践上默认 interface,需要运算类型时用 type。

面试官:any 和 unknown 差在哪?

你:都能接收任意值,但 any 之后对它的任何操作都放行,等于关闭类型检查;unknown 是类型安全版,使用前必须先收窄——通过 typeof、instanceof、自定义类型守卫或 discriminate narrowing 证明类型后才能操作。所以 unknown 适合"外部输入"(接口响应、用户输入、localStorage 反序列化),any 适合"明知放弃"的逃生舱。

面试官:写一个 MyReturnType,说说 infer 的原理。

你:type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never。infer 是条件类型里的类型占位符:当 F 的结构能匹配 (...args: any[]) => R 这个模式时,返回位置的类型被捕获绑定到 R 上并返回。同类用法还有从数组元素 infer T[] 提取 T、从 Promise<T> 提取 T。注意裸类型参数参与条件类型时有分发性,联合类型会被逐个处理,不想要分发就用方括号包起来 [T] extends [U]。