原始类型(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);
}
本章小结
本章核心要点
- 原始类型:string/number/boolean/null/undefined/symbol/bigint 是 TypeScript 的七种原始类型;null 和 undefined 在 strict 模式下是独立类型,不再是所有类型的子类型。
- any vs unknown vs never:any 关闭类型检查(应避免);unknown 是安全的未知类型(必须缩窄后才能使用);never 是不可能的类型(用于穷举检查和过滤条件类型中的成员)。
- 元组 vs 数组:数组是同类型可变长集合;元组是固定长度、每位置类型明确的结构;React hooks 如 useState 返回的是元组 [state, setState]。
- 字面量类型与类型拓宽:let 声明的变量类型会被拓宽('hello' → string);const 声明或 as const 标注会保留字面量类型;字面量联合类型是构建可辨识联合的基础。
- 类型断言的风险:as 只是编译时提示,运行时无任何保护;应优先使用类型守卫(is 关键字)进行实际类型检查,而不是用 as 强行绕过检查。