Skip to content

第 09 章 · 面试题集(TypeScript 基础类型)


Q1:TypeScript 和 JavaScript 的关系是什么?为什么要用 TS?

考察点:TS 的本质、价值定位

标准答案: TypeScript 是 JavaScript 的超集——所有合法的 JS 都是合法的 TS。它在 JS 之上加了一套静态类型系统,编译时进行类型检查,最终编译产物仍然是纯 JS。 价值:

  1. 编译时发现类型错误,减少线上 bug
  2. 类型即文档,提升团队协作效率
  3. IDE 支持极强(自动补全、跳转、重构)
  4. 中大型项目的事实标准

通俗理解:JS 是"开车不看仪表盘,撞了再说";TS 是"开车前先体检+车况检查"。

代码示例

ts
function add(a: number, b: number): number {
  return a + b;
}
add(1, "2"); // ❌ 编译报错,立刻发现

加分回答

  • TS 是类型擦除型语言:编译后没有任何运行时类型信息,性能与 JS 一致
  • TS 提供 strict 模式(包含 strictNullChecksnoImplicitAny 等),强烈建议开启
  • TS 不强制全量迁移,可以从 allowJs 开始渐进引入

追问

  • TS 编译后的 JS 性能比手写 JS 慢吗?(不会,类型擦除)
  • 如果一个第三方库没有 d.ts 怎么办?(先 declare module "xxx" 兜底,或自己写声明)

Q2:anyunknownnever 的区别?

考察点:顶层类型与底层类型的理解

标准答案

类型含义能否赋值给别的类型别的类型能否赋给它
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 类型系统的"特例"
  • voidundefined 的区别:void 只用于函数返回值"不关心",可以赋值 undefined,但不能反过来

追问

  • 为什么有了 any 还要 unknown?(安全;避免类型污染)
  • 怎么写穷尽检查?(用 never 接住未覆盖的分支)

Q3:interface 和 type 的区别?什么时候用哪个?

考察点:接口 vs 类型别名

标准答案

维度interfacetype
描述对象/类
描述联合/交叉/原始类型
同名声明合并✅ 自动合并❌ 报错
继承语法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:使用方可以通过声明合并扩展全局类型(比如 WindowReact.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 种:

  1. typeof:用于原始类型 string/number/boolean/symbol/undefined/object/function
  2. instanceof:用于类
  3. in:检查对象是否含某属性
  4. 自定义守卫(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 functionfunction 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 和类型转换的区别?什么时候必须用?

考察点:类型断言的本质

标准答案类型断言只是告诉编译器"我比你更清楚",不会做任何运行时转换。它和强类型语言的"类型转换"完全不同。 必须用的场景通常有:

  1. DOM API 返回的类型过宽(如 getElementById 返回 HTMLElement | null
  2. 与第三方库交互时,临时绕过类型系统
  3. as const 的字面量收窄
  4. 泛型函数推断不出来时手动指定

通俗理解

  • 类型断言 = 跟海关说"这真的是水"——错了自己负责
  • 类型转换(如 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

追问

  • asas unknown as 的区别?(前者要求源/目标有重叠,后者强制绕过)
  • satisfies 怎么用?(const config = { ... } satisfies AppConfig

Q9(加餐):readonlyconstReadonly<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>,下一章会讲)