TypeScript 基础类型系统

掌握 TypeScript 原始类型、接口、类型别名、联合类型与字面量类型的工程用法,建立类型思维的基础。

TypeScript 基础类型系统

为什么类型系统是工程基础

TypeScript 的核心价值不在于"给 JavaScript 加类型",而在于通过静态分析将一大类运行时错误提前到编译阶段暴露。在大型前端项目中,类型系统充当着可执行的文档和重构安全网的双重角色。

面试定位:基础类型系统是 TypeScript 面试的入门门槛。面试官通过 interface vs type、联合类型的收窄方式等问题,快速判断候选人是否真正理解类型系统,还是仅停留在 any 和类型断言的层面。


原始类型与类型标注

JavaScript 原始类型在 TypeScript 中的映射

TypeScript 为 JavaScript 的七种原始类型提供一对一映射:

const name: string = "Alice";
const age: number = 30;
const active: boolean = true;
const id: bigint = 9007199254740991n;
const key: symbol = Symbol("key");
const empty: null = null;
const missing: undefined = undefined;

工程要点

  • 始终使用小写形式(string 而非 String)。大写形式指向的是 JavaScript 包装对象,几乎没有正当的使用场景。
  • nullundefined 在开启 strictNullChecks 后成为独立类型,不再隐式赋值给其他类型。这是 TypeScript 类型安全的基石之一。

数组与元组

// 数组:两种等价写法
const scores: number[] = [90, 85, 92];
const names: Array<string> = ["Alice", "Bob"];

// 元组:固定长度和位置类型
const pair: [string, number] = ["age", 30];
const rgb: [number, number, number] = [255, 128, 0];

// 带标签的元组(TypeScript 4.0+),提升可读性
type Point = [x: number, y: number, z: number];

any / unknown / never / void

这四个特殊类型构成了 TypeScript 类型系统的边界:

类型 语义 赋值方向 适用场景
any 放弃类型检查 可赋值给任何类型,也可接受任何值 渐进迁移遗留代码
unknown 类型安全的顶层类型 可接受任何值,但使用前必须收窄 处理外部输入
never 不可能存在的值 是所有类型的子类型,没有值可以赋给它 exhaustive check、抛出异常
void 无返回值 仅用于函数返回 副作用函数
// unknown 强制收窄
function processInput(input: unknown): string {
  if (typeof input === "string") return input.toUpperCase();
  if (typeof input === "number") return input.toFixed(2);
  throw new Error("Unsupported input type");
}

// never 用于 exhaustive check
type Shape = "circle" | "square";
function getArea(shape: Shape): number {
  switch (shape) {
    case "circle": return Math.PI * 10 ** 2;
    case "square": return 10 ** 2;
    default:
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

Interface 与 Type Alias

核心区别

两者在大多数场景下可以互换,但存在关键差异:

// Interface:声明合并(Declaration Merging)
interface User {
  name: string;
}
interface User {
  age: number;
}
// User 自动合并为 { name: string; age: number }

// Type Alias:不支持声明合并,但支持更多类型运算
type Result = Success | Failure;
type Coords = [number, number];
type Callback = (event: Event) => void;

选择原则

场景 推荐 原因
对象形状定义 interface 支持 extends 继承、声明合并,更适合面向对象的 API 设计
联合类型、交叉类型、元组 type interface 无法表达这些类型运算
第三方库的类型扩展 interface 声明合并能力使用户无需修改源码即可扩展类型
函数类型 两者皆可 type 的写法通常更简洁

extends vs 交叉类型

// Interface 继承
interface Animal { name: string }
interface Dog extends Animal { breed: string }

// Type 交叉
type Animal = { name: string };
type Dog = Animal & { breed: string };

两者在大多数情况下等价,但在类型冲突时行为不同:extends 会报错,而 & 会产生 never


联合类型与交叉类型

联合类型(Union Types)

联合类型表示"A 或 B"的关系,是 TypeScript 中最常用的类型组合方式:

type Status = "idle" | "loading" | "success" | "error";
type ID = string | number;

function formatId(id: ID): string {
  // 使用前必须收窄
  if (typeof id === "string") return id.toUpperCase();
  return id.toString();
}

判别联合类型(Discriminated Unions)

这是 TypeScript 中最强大的模式之一,通过共享的字面量属性实现类型安全的分支处理:

type ApiResponse =
  | { status: "success"; data: unknown }
  | { status: "error"; message: string }
  | { status: "loading" };

function handleResponse(res: ApiResponse) {
  switch (res.status) {
    case "success":
      console.log(res.data);  // TypeScript 知道 data 存在
      break;
    case "error":
      console.error(res.message);  // TypeScript 知道 message 存在
      break;
    case "loading":
      break;
  }
}

交叉类型(Intersection Types)

交叉类型表示"A 且 B"的关系,用于组合多个类型:

type Timestamped = { createdAt: Date; updatedAt: Date };
type SoftDeletable = { deletedAt: Date | null };

type BaseEntity = Timestamped & SoftDeletable;
// { createdAt: Date; updatedAt: Date; deletedAt: Date | null }

字面量类型与 const 断言

字面量类型

TypeScript 允许将具体的值作为类型使用,这是联合类型威力的基础:

type Direction = "north" | "south" | "east" | "west";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

as const 断言

as const 将值的类型收窄到最精确的字面量形式:

// 没有 as const
const config = { endpoint: "/api", method: "GET" };
// 类型:{ endpoint: string; method: string }

// 使用 as const
const config = { endpoint: "/api", method: "GET" } as const;
// 类型:{ readonly endpoint: "/api"; readonly method: "GET" }

// 常见模式:从对象推导联合类型
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"

枚举(Enum)与替代方案

数值枚举与字符串枚举

// 字符串枚举
enum Direction {
  North = "NORTH",
  South = "SOUTH",
}

// 数值枚举(不推荐:容易出现反向映射陷阱)
enum Status {
  Active,  // 0
  Inactive, // 1
}

为什么现代项目倾向避免 enum

  1. Tree-shaking 问题:数值枚举编译为 IIFE,bundler 无法消除未使用的成员
  2. 运行时开销const enum 虽然内联,但在 isolatedModules 模式(Vite、esbuild)下行为不一致
  3. 联合类型替代方案更轻量
// 推荐:联合类型 + as const 对象
const Direction = {
  North: "NORTH",
  South: "SOUTH",
} as const;

type Direction = (typeof Direction)[keyof typeof Direction];
// "NORTH" | "SOUTH"

类型断言与类型守卫

类型断言

类型断言是告诉编译器"我比你更了解这个值的类型":

const canvas = document.getElementById("main") as HTMLCanvasElement;
const ctx = canvas.getContext("2d")!; // 非空断言

工程纪律:类型断言绕过了类型检查,应视为技术债务。每个 as 都应有注释说明理由,团队应通过 lint 规则限制其使用频率。

基础类型守卫

function isString(value: unknown): value is string {
  return typeof value === "string";
}

// 使用
function process(input: unknown) {
  if (isString(input)) {
    console.log(input.toUpperCase()); // TypeScript 知道是 string
  }
}

函数类型

参数与返回值标注

// 函数声明
function greet(name: string, age?: number): string {
  return age ? `${name}, ${age}` : name;
}

// 函数类型表达式
type Comparator<T> = (a: T, b: T) => number;

// 可调用接口
interface StringParser {
  (input: string): number;
  name: string;
}

函数重载

function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: "span"): HTMLSpanElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

面试高频问题

Q: interface 和 type 有什么区别?如何选择?

回答要点:两者在对象类型定义上几乎等价。核心区别在于 interface 支持声明合并(适合库的类型扩展)和 extends 继承链,而 type alias 支持联合、交叉、元组等类型运算。在团队项目中,通常约定对象形状用 interface、类型运算用 type,保持一致性比选择本身更重要。

Q: any 和 unknown 的区别是什么?

回答要点any 完全放弃类型检查,值可以任意使用而不报错。unknown 是类型安全的顶层类型——可以接受任何值,但使用前必须通过类型守卫或断言收窄。在处理外部数据(API 响应、JSON 解析)时,unknown 是正确选择,它迫使开发者显式处理类型不确定性。

Q: 什么是判别联合类型?有什么实际用途?

回答要点:判别联合类型是一组共享某个字面量属性(判别属性)的对象类型的联合。TypeScript 通过检查判别属性的值来收窄类型。这在处理 API 响应、状态机、Redux action 等场景中非常实用——它让编译器帮你确保每个分支都正确处理了所有可能的情况。


延展阅读