Chapter 02

基础类型:primitive、联合与交叉

掌握 TypeScript 所有基础类型、any/unknown/never 的使用场景与联合交叉类型

原始类型(Primitive Types)

TypeScript 的类型系统建立在 JavaScript 运行时值的基础上。每种原始类型对应 JavaScript 中 typeof 返回的一种值类别。理解这些类型的边界和特殊行为,是写出正确类型标注的基础。

七种基础原始类型

TypeScript 支持 JavaScript 中所有的原始类型,加上几个 TypeScript 特有的类型:

// ── 字符串 ──
let name: string = '张三';
let template: string = `Hello, ${name}`;

// ── 数字(统一,没有 int/float 区分)──
let age: number = 28;
let price: number = 99.99;
let hex: number = 0xff;

// ── 布尔 ──
let isActive: boolean = true;

// ── null 和 undefined(开启 strictNullChecks 后严格区分)──
let n: null = null;
let u: undefined = undefined;

// ── Symbol ──
const sym1: symbol = Symbol('key');
const sym2: unique symbol = Symbol('unique');  // 更严格的符号类型

// ── BigInt(ES2020)──
let big: bigint = 9007199254740991n;

any、unknown 和 never

这三个类型是 TypeScript 类型系统中的"特殊成员",分别代表三种极端情况:any 是对类型系统的完全放弃,unknown 是对未知类型的安全处理,never 是不可能存在的类型。正确使用它们,能让你的代码既灵活又安全。

any
类型系统的"关闭开关"。赋值给 any 或从 any 赋值都不检查类型。any 会"传染"——接受 any 的操作结果也是 any。使用场景:迁移旧 JS 代码、第三方库无类型声明的临时处理。应尽量避免,优先考虑 unknown。
unknown
类型安全的 any 替代。与 any 的区别:unknown 类型的值不能直接使用,必须先通过类型缩窄(typeof/instanceof/类型断言)确认类型后才能操作。适合表示"我不知道这个值的类型,但我会在使用前检查"的场景,如 fetch 返回值、catch 错误对象。
never
空类型,没有任何值属于 never。出现场景:函数永不返回(只抛异常或无限循环)、类型缩窄后不可能到达的分支、条件类型中用来"过滤"联合成员。never 是所有类型的子类型,但没有类型是 never 的子类型(never 本身除外)。

any:类型系统的逃生门(谨慎使用)

let x: any = 'hello';
x = 42;       // OK
x = true;     // OK
x.foo();      // OK(TypeScript 不检查,运行时可能崩溃!)
x[0].bar();   // OK(同上)

// any 会"传染":接受 any 的操作结果也是 any
const result = x + 1;  // result 类型是 any

unknown:类型安全的 any 替代

let value: unknown = fetchSomething();

// 不先检查类型,无法使用
value.toUpperCase();  // ❌ 编译错误:Object is of type 'unknown'
value + 1;            // ❌ 编译错误

// 必须先缩窄类型才能使用
if (typeof value === 'string') {
  value.toUpperCase();  // ✅ OK,此处 value 是 string
}

// 最佳实践:处理不确定类型的数据(如 API 响应)用 unknown 而非 any
async function fetchUser(): Promise<unknown> {
  const res = await fetch('/api/user');
  return res.json();
}

never:不可能发生的类型

// never 表示永远不会有值的类型

// 1. 函数永不返回(抛出错误或无限循环)
function throwError(msg: string): never {
  throw new Error(msg);
}

// 2. 穷举检查:确保 switch 处理了所有情况
type Shape = { kind: 'circle'; radius: number }
           | { kind: 'rect'; w: number; h: number };

function area(s: Shape): number {
  switch (s.kind) {
    case 'circle': return Math.PI * s.radius ** 2;
    case 'rect':   return s.w * s.h;
    default: return assertNever(s);  // 未处理的情况会编译错误
  }
}
function assertNever(x: never): never {
  throw new Error('未处理的情况: ' + JSON.stringify(x));
}

类型系统中的 Top 和 Bottom 类型

TypeScript 的类型层级图中,有几个特殊的位置:

Top 类型(unknown / any)
类型层级的顶部。所有类型都是 unknown 的子类型(可以赋值给 unknown);所有类型也都是 any 的子类型(可以赋值给 any)。unknown 和 any 的区别在于,any 也是所有类型的子类型(any 可以赋值给任何类型),而 unknown 不行——这是 any 危险性的根源。
Bottom 类型(never)
类型层级的底部。never 是所有类型的子类型(never 可以赋值给任何类型),但没有任何类型是 never 的子类型(除 never 自身)。这意味着"never 类型的值可以安全地视为任何类型"——但正因为 never 不存在任何值,这个规则实际上从不会被触发。
void 与 undefined 的微妙差异
void 表示"我不关心这个函数的返回值",undefined 表示"这个函数明确返回 undefined"。关键区别:类型为 () => void 的回调可以返回任何值(TypeScript 忽略它),这是为了兼容 [1,2,3].forEach(n => n * 2) 这类代码;而类型为 () => undefined 的函数必须显式 return undefined。
// 类型层级示意
// unknown / any   ← Top(所有类型都可赋值给这两个)
//   ↑
// string | number | boolean | object | ...  ← 普通类型
//   ↑
// 'hello' | 42 | true  ← 字面量类型(具体类型的子集)
//   ↑
// never  ← Bottom(never 可以赋值给所有类型,但没有值属于 never)

// any 的双向性(危险):
let a: any = 'hello';
let n: number = a;   // ✅ 编译通过(any 赋给 number)→ 运行时错误风险

// unknown 的单向性(安全):
let u: unknown = 'hello';
let n2: number = u;  // ❌ 编译错误!必须先缩窄类型
let n3: number = u as number;  // 必须明确使用断言才能通过

数组和元组

数组类型和元组类型在 TypeScript 中有重要区别:数组是同类型元素的可变长度集合,元组是固定长度、每个位置类型明确的结构。元组常用于函数多返回值、CSV 行数据等场景。

// 数组:两种等价语法
const nums1: number[] = [1, 2, 3];
const nums2: Array<number> = [1, 2, 3];
const strs: string[] = ['a', 'b', 'c'];
const mixed: (string | number)[] = ['hello', 42];

// 元组:固定长度和类型的数组
type Point = [number, number];
const pt: Point = [3, 4];

type Named = [name: string, age: number];  // 具名元组(TS 4.0+)
const person: Named = ['张三', 28];

// 元组解构
const [x, y] = pt;   // x: number, y: number
const [n, a] = person;  // n: string, a: number

// 可选元素和剩余元素
type Flexible = [string, ...number[]];  // 第一个是 string,其余都是 number

联合类型与交叉类型

// 联合类型(Union):OR 语义
type StringOrNumber = string | number;
type Status = 'loading' | 'success' | 'error';  // 字面量联合

function format(value: StringOrNumber): string {
  if (typeof value === 'string') {
    return value.toUpperCase();
  }
  return value.toFixed(2);
}

// 交叉类型(Intersection):AND 语义
type HasName = { name: string };
type HasAge = { age: number };
type Person = HasName & HasAge;  // 必须同时有 name 和 age

const p: Person = { name: '李四', age: 30 };  // ✅
const p2: Person = { name: '王五' };           // ❌ 缺少 age

// 交叉类型常用于 Mixin 模式
type Serializable = { serialize: () => string };
type Loggable = { log: () => void };
type SerializableLogger = Serializable & Loggable;

字面量类型与 as const

// 字面量类型:值本身作为类型
type Direction = 'left' | 'right' | 'up' | 'down';

function move(dir: Direction) { /* ... */ }
move('left');     // ✅
move('diagonal'); // ❌

// as const:将对象/数组的类型缩窄为字面量类型
const config = {
  host: 'localhost',
  port: 3000,
} as const;
// config.host 类型是 'localhost'(不是 string)
// config.port 类型是 3000(不是 number)
// config 整体是 readonly 的

const ROUTES = ['/home', '/about', '/contact'] as const;
type Route = typeof ROUTES[number];  // '/home' | '/about' | '/contact'
类型拓宽(Type Widening)与字面量类型的关系

let x = 'hello' 中 x 的类型是 string(TypeScript 自动拓宽为更宽的类型,因为 let 变量可以重新赋值);const x = 'hello' 中 x 的类型是 'hello'(字面量类型,因为 const 不可重赋值,TypeScript 保留了最精确的类型)。当你需要保留字面量类型时,要么用 const,要么加 as const 或显式类型标注。

void 和 object 类型

// void:函数没有返回值时使用(不是 undefined,但可以赋 undefined)
function printLog(msg: string): void {
  console.log(msg);
  // 不 return,或 return; 或 return undefined; 都 OK
}

// void vs undefined 的区别
type Callback = () => void;
// 类型为 void 的回调可以返回任何值,TypeScript 会忽略它
// 这是为了兼容如 [1,2,3].forEach(n => n * 2) 这样的代码
// (Array.prototype.forEach 的回调类型是 () => void)

// object:排除所有原始类型,代表任何对象/数组/函数
function processObject(obj: object): void {
  // 无法访问任何属性,因为 object 类型没有属性信息
  // 通常应该使用更具体的类型,object 很少直接用
}

// 更实用的做法:用 Record 或接口代替 object
function logKeys(obj: Record<string, unknown>): void {
  console.log(Object.keys(obj));  // OK
}

类型断言(Type Assertions)与类型守卫

// as 断言:告诉 TypeScript "相信我,这个类型是 T"
const input = document.getElementById('search') as HTMLInputElement;

// 注意:as 只是在编译时欺骗了 TypeScript,运行时没有任何保护
const num = 'hello' as unknown as number;  // 双重断言,强行绕过检查(危险!)
console.log(num.toFixed(2));  // 运行时会崩溃

// 正确做法:用类型守卫进行实际类型检查
function isHTMLInputElement(el: Element): el is HTMLInputElement {
  return el.tagName === 'INPUT';
}

const el = document.getElementById('search');
if (el && isHTMLInputElement(el)) {
  // 安全:此处 el 已被确认是 HTMLInputElement
  console.log(el.value);
}

本章小结

本章核心要点