主题
第 09 章 · 面试题集(TypeScript 基础类型)
Q1:TypeScript 和 JavaScript 的关系是什么?为什么要用 TS?
考察点:TS 的本质、价值定位
标准答案: TypeScript 是 JavaScript 的超集——所有合法的 JS 都是合法的 TS。它在 JS 之上加了一套静态类型系统,编译时进行类型检查,最终编译产物仍然是纯 JS。 价值:
- 编译时发现类型错误,减少线上 bug
- 类型即文档,提升团队协作效率
- IDE 支持极强(自动补全、跳转、重构)
- 中大型项目的事实标准
通俗理解:JS 是"开车不看仪表盘,撞了再说";TS 是"开车前先体检+车况检查"。
代码示例:
ts
function add(a: number, b: number): number {
return a + b;
}
add(1, "2"); // ❌ 编译报错,立刻发现加分回答:
- TS 是类型擦除型语言:编译后没有任何运行时类型信息,性能与 JS 一致
- TS 提供
strict模式(包含strictNullChecks、noImplicitAny等),强烈建议开启 - TS 不强制全量迁移,可以从
allowJs开始渐进引入
追问:
- TS 编译后的 JS 性能比手写 JS 慢吗?(不会,类型擦除)
- 如果一个第三方库没有 d.ts 怎么办?(先
declare module "xxx"兜底,或自己写声明)
Q2:any、unknown、never 的区别?
考察点:顶层类型与底层类型的理解
标准答案:
| 类型 | 含义 | 能否赋值给别的类型 | 别的类型能否赋给它 |
|---|---|---|---|
any | 关闭类型检查 | 可以 | 可以 |
unknown | 类型安全的 any,必须类型守卫后才能用 | 不能(除 any/unknown) | 可以 |
never | 永远不会出现的值(异常/死循环的返回值) | 可以赋给任何类型 | 不能 |
通俗理解:
any:保安睡了,谁都能进unknown:保安严格,进门要查身份证never:根本没人来,比如"火星人办护照"
代码示例:
ts
let a: any = 1; a.foo(); // ✅ 编译通过(运行时可能炸)
let u: unknown = 1; u.toFixed(); // ❌ 必须先 typeof 守卫
function fail(msg: string): never {
throw new Error(msg); // 永远不返回
}
// never 在穷尽检查中很有用
type Shape = { kind: "circle" } | { kind: "square" };
function area(s: Shape) {
switch (s.kind) {
case "circle": return 1;
case "square": return 2;
default: const _exhaustive: never = s; // 漏分支会报错
}
}加分回答:
never是所有类型的子类型(bottom type)any是所有类型的"祖宗",也是所有类型的"儿子"——这是 TS 类型系统的"特例"void与undefined的区别:void只用于函数返回值"不关心",可以赋值undefined,但不能反过来
追问:
- 为什么有了
any还要unknown?(安全;避免类型污染) - 怎么写穷尽检查?(用
never接住未覆盖的分支)
Q3:interface 和 type 的区别?什么时候用哪个?
考察点:接口 vs 类型别名
标准答案:
| 维度 | interface | type |
|---|---|---|
| 描述对象/类 | ✅ | ✅ |
| 描述联合/交叉/原始类型 | ❌ | ✅ |
| 同名声明合并 | ✅ 自动合并 | ❌ 报错 |
| 继承语法 | extends | & |
| 实现(implements) | 类可以实现 | 类也可实现 |
经验法则:
- 描述"对象/类的形状" → 优先
interface(可被合并、可被扩展) - 描述"联合/交叉/工具类型/复杂组合" → 用
type
通俗理解:
interface像乐高底板——可以无限拼新模块type像配方表——一次定型,不能改
代码示例:
ts
interface User { name: string }
interface User { age: number } // ✅ 合并为 { name; age }
type ID = string | number; // type 才能写联合
type Admin = User & { role: "admin" };
class A implements User { name = "a"; age = 1; }加分回答:
- 库作者强烈推荐 interface:使用方可以通过声明合并扩展全局类型(比如
Window、React.HTMLAttributes) - 性能层面:编译器对 interface 缓存更好(大型项目稍快),但实际差异极小
type可以用映射类型/条件类型做"类型计算",interface 不行
追问:
- 怎么扩展全局
Window?(declare global { interface Window { __APP__: any } }) - type 能不能继承 interface?(可以用交叉
type X = I & { ... })
Q4:什么是字面量类型?怎么用它代替枚举?
考察点:字面量类型 + as const 的现代用法
标准答案: 字面量类型把"具体的值"当作"类型本身"。例如 "red" 可以是一个类型,仅表示这一个字符串。配合联合类型可以构成比 enum 更轻量、更安全的枚举替代方案。
通俗理解:枚举 = "可选答案 A/B/C/D";字面量联合 = "题干已经把答案打印好"。
代码示例:
ts
// 字面量联合
type Direction = "up" | "down" | "left" | "right";
// as const 把对象冻结为字面量
const Status = { Open: "OPEN", Closed: "CLOSED" } as const;
type Status = typeof Status[keyof typeof Status]; // "OPEN" | "CLOSED"
function move(d: Direction) {}
move("up"); // ✅
move("north"); // ❌加分回答:
enum会生成额外运行时代码(数字枚举甚至有双向映射),打包体积变大const enum可以内联,但要求关闭isolatedModules,与 Babel/Vite 不太兼容- 团队规范里推荐用
as const+ 字面量联合 替代enum
追问:
as const做了什么?(把所有属性变只读 + 字面量类型)- 字面量类型可以是 number/boolean 吗?(可以:
type ZeroOrOne = 0 | 1)
Q5:什么是类型守卫?有几种写法?
考察点:类型收窄
标准答案: 类型守卫让 TS 在某个分支内自动收窄变量类型。常见 4 种:
typeof:用于原始类型string/number/boolean/symbol/undefined/object/functioninstanceof:用于类in:检查对象是否含某属性- 自定义守卫(user-defined type guard):函数返回
参数 is 类型
通俗理解:类型守卫 = 机场分流通道——一旦你走进"国内通道",所有后续检查都按"国内"标准来。
代码示例:
ts
// 1. typeof
function len(x: string | number) {
if (typeof x === "string") return x.length;
return x.toString().length;
}
// 2. instanceof
function logErr(e: Error | string) {
if (e instanceof Error) console.log(e.stack);
}
// 3. in
type Cat = { meow(): void };
type Dog = { bark(): void };
function speak(p: Cat | Dog) {
if ("meow" in p) p.meow(); else p.bark();
}
// 4. 自定义守卫
function isCat(p: Cat | Dog): p is Cat {
return (p as Cat).meow !== undefined;
}加分回答:
Array.isArray()在 TS 内置守卫为value is any[]- 自定义守卫常配合
discriminated union(带 kind 的联合类型)做"穷尽分发" - 还有
assertion function:function assertString(x: unknown): asserts x is string,断言失败抛错,断言成功后续自动收窄
追问:
typeof null是什么?("object",著名历史 bug,需要单独判 null)- 自定义守卫返回 false 会怎样?(不会抛错,只是不收窄类型)
Q6:联合类型和交叉类型的区别?
考察点:| 与 & 的语义
标准答案:
- 联合
A | B:值是 A 或 B 之一("或") - 交叉
A & B:值同时满足 A 和 B 的所有约束("与")
注意:
- 对象交叉 = 字段合并
- 原始类型交叉常常是
never(如string & number = never)
通俗理解:
- 联合 = "可乐或雪碧"(点其一)
- 交叉 = "VIP 卡 + 学生证"(必须两证齐全)
代码示例:
ts
type Id = string | number;
const a: Id = "u_1"; const b: Id = 100;
interface Named { name: string }
interface Aged { age: number }
type Person = Named & Aged;
const p: Person = { name: "A", age: 1 };
type Impossible = string & number; // never加分回答:
- 联合类型在使用时必须类型守卫才能访问"非公共属性"
- 交叉类型可用于 mixin 模式:
type WithLog<T> = T & { log(): void } - React 里
Props & React.HTMLAttributes<HTMLDivElement>是常见交叉
追问:
- 为啥
string & number是 never?(类型系统找不到一个值同时是字符串和数字) - 联合类型如何"分发"?(
T extends U ? X : Y在 T 是裸类型参数时会自动分发)
Q7:函数的可选参数、默认参数、剩余参数怎么写?
考察点:函数类型签名
标准答案:
ts
// 可选参数 ? 必须放在最后
function greet(name: string, title?: string) {}
// 默认参数 = 自动可选
function pow(x: number, n: number = 2) {}
// 剩余参数 ... 必须是最后一个,且类型为数组
function sum(...nums: number[]) {}
// 完整函数类型
type Handler = (event: string, data?: any) => void;
// 重载(在调用方根据入参形态推断不同返回)
function pick(arr: string[]): string;
function pick(arr: number[]): number;
function pick(arr: any[]): any { return arr[0]; }通俗理解:
- 可选
?= "户口本带不带都行" - 默认
== "不交照片就用证件照库存" - 剩余
...= "随便多带几件行李塞行李箱"
代码示例:见上。
加分回答:
- TS 4.0+ 支持可变元组,剩余参数可以放中间:
function f<T extends any[]>(a: string, ...args: [...T, number]) - 函数重载在 TS 中是编译期的,运行时只有最后那个实现
- 推荐优先用联合类型 + 守卫而不是重载,更易维护
追问:
- 默认参数和可选参数都能省略,差别是什么?(默认参数有值;可选参数是 undefined)
- 为啥剩余参数必须是数组?(运行时它就是
arguments之外的数组)
Q8:类型断言 as 和类型转换的区别?什么时候必须用?
考察点:类型断言的本质
标准答案: 类型断言只是告诉编译器"我比你更清楚",不会做任何运行时转换。它和强类型语言的"类型转换"完全不同。 必须用的场景通常有:
- DOM API 返回的类型过宽(如
getElementById返回HTMLElement | null) - 与第三方库交互时,临时绕过类型系统
as const的字面量收窄- 泛型函数推断不出来时手动指定
通俗理解:
- 类型断言 = 跟海关说"这真的是水"——错了自己负责
- 类型转换(如
Number("1"))= 真把它换成数字
代码示例:
ts
const el = document.getElementById("app") as HTMLDivElement;
el.innerText = "Hi";
// 双重断言(绕过宽窄检查,慎用)
const x = "abc" as unknown as number;
// 非空断言 ! (也是断言的一种)
const user = findUser()!; // 我保证不为 null
// as const
const arr = [1, 2, 3] as const; // readonly [1, 2, 3]加分回答:
- TS 4.9 引入的
satisfies操作符比as更安全:保留字面量类型的同时校验是否符合接口 <Type>value是旧语法,与 JSX 冲突,统一推荐value as Type- 滥用
as等于关闭类型系统,建议在 ESLint 中加@typescript-eslint/no-unsafe-assertion
追问:
as和as unknown as的区别?(前者要求源/目标有重叠,后者强制绕过)satisfies怎么用?(const config = { ... } satisfies AppConfig)
Q9(加餐):readonly、const 和 Readonly<T> 有何区别?
考察点:不可变性的几个层次
标准答案:
const:变量绑定不可变(但对象内容可改)readonly:对象字段不可改Readonly<T>:把整个对象的所有字段一次性变只读as const:把字面量"冻结"为最窄类型 + 全部 readonly
代码示例:
ts
const arr = [1, 2]; arr.push(3); // ✅ 内容可改
interface User { readonly id: number; name: string }
const u: User = { id: 1, name: "A" };
u.id = 2; // ❌
type R = Readonly<{ a: number; b: number }>;
// = { readonly a: number; readonly b: number }
const tuple = [1, "a"] as const; // readonly [1, "a"]追问:
Readonly<T>是浅只读还是深只读?(浅)- 怎么写深只读?(自己写
DeepReadonly<T>,下一章会讲)