| 类型 | 写法 | 说明 |
|---|
| 字符串 | string | UTF-16 序列 |
| 数字 | number | 双精度浮点,没有 int |
| 布尔 | boolean | true / false |
| 大整数 | bigint | 123n,需 target ≥ ES2020 |
| 符号 | symbol | Symbol() |
| 空 | null | 需 strictNullChecks(6.0 默认开)才与其它类型区分 |
| 未定义 | undefined | 同上 |
| 永不存在的值 | never | 空集,见下节 |
⚠️ 包装对象类型不是原始类型,这是最高频的新手错误:
1
2
3
| const a: string = "x"; // ✅ 原始类型(永远用这个)
const b: String = "x"; // ⚠️ 合法但是包装对象,别用
const c: string = new String("x"); // ❌ TS2322: 'String' is not assignable to 'string'
|
| 🛑 别用 | ✅ 用 | 原因 |
|---|
String | string | 包装对象类型几乎总是写错了 |
Number | number | 同上 |
Boolean | boolean | 同上 |
Symbol | symbol | 同上 |
BigInt | bigint | 同上 |
Object | object 或具体形状 | Object 几乎接受一切,等于没约束 |
💭 例外:极少数库的 API 确实要求包装对象类型(因为要挂方法),照文档写即可,但自己定义类型时永远用小写。
字面量本身就是类型,表示「只能是这一个值」。
1
2
3
4
5
6
| type Direction = "up" | "down"; // 字符串字面量联合——最常用的建模手段 🔥
type Dice = 1 | 2 | 3 | 4 | 5 | 6;
type Yes = true;
const a: Direction = "up"; // ✅
const b: Direction = "left"; // ❌ TS2322: '"left"' is not assignable to '"up" | "down"'
|
字面量何时会被「拓宽」——这是最容易困惑的地方:
| 写法 | 推导出的类型 | 原因 |
|---|
const x = "a" | "a" | const 不能重新赋值,保持最窄 |
let x = "a" | string | let 可能被改,拓宽为 string |
const o = { k: "a" } | { k: string } | 对象属性会被拓宽 ⚠️ |
const o = { k: "a" } as const | { readonly k: "a" } | as const 阻止拓宽 🔥 |
const arr = [1, 2] | number[] | 数组元素被拓宽 |
const arr = [1, 2] as const | readonly [1, 2] | 变成只读元组 |
1
2
3
4
5
6
7
8
9
| // as const 的典型用途:让配置对象保留精确字面量
const config = {
host: "localhost",
port: 8080,
mode: "dev",
} as const;
type Mode = typeof config.mode; // "dev"(而不是 string)
type Port = typeof config.port; // 8080(而不是 number)
|
模板字面量类型(TS 4.1+):在类型层面做字符串拼接。
1
2
3
4
5
6
7
8
9
10
11
| type Greeting = `hello ${string}`;
const g1: Greeting = "hello world"; // ✅
const g2: Greeting = "goodbye"; // ❌ TS2322
// 组合内置字符串工具类型
type EventName = "click" | "focus";
type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus"
// 解析字符串结构(配合 infer,见 05)
type ExtractRouteParam<T> = T extends `${string}:${infer P}` ? P : never;
type P = ExtractRouteParam<"/user/:id">; // "id"
|
symbol 的类型层面细化,用于保证唯一性,是品牌类型(branded type)的基石之一。
1
2
3
4
5
| const KEY: unique symbol = Symbol("key"); // 必须 const + 直接初始化
type KEY = typeof KEY; // 类型是 typeof KEY,不是 symbol
const other: symbol = Symbol("key");
const bad: typeof KEY = other; // ❌ TS2322: symbol 不能赋给 unique symbol
|
1
2
3
4
5
6
7
8
| // 用 unique symbol 做「名义类型」标记
declare const brand: unique symbol;
type UserId = string & { readonly [brand]: "UserId" };
type OrderId = string & { readonly [brand]: "OrderId" };
declare const uid: UserId;
declare const oid: OrderId;
const x: OrderId = uid; // ❌ TS2322: 两者结构不同,无法互赋 🔥
|
| 类型 | 含义 | 典型位置 |
|---|
void | 函数没有有意义的返回值 | 回调返回值位置 |
undefined | 值不存在 | 可选属性、未初始化变量 |
null | 显式的空值 | API 返回、DOM 查询 |
⚠️ void 的特例:() => void 作为回调类型时,允许传入有返回值的函数。
1
2
3
4
5
6
7
8
9
10
| type Callback = () => void;
const returnsNumber = () => 42;
const cb: Callback = returnsNumber; // ✅ 合法!
// 原因:调用方声明「我不关心返回值」,实现返回什么都无害
[1, 2, 3].forEach((n) => n * 2); // ✅ 箭头函数返回了值,照样合法
// 但直接声明返回值时必须一致
function f(): void { return 42; } // ❌ TS2322: Type 'number' is not assignable to type 'void'
|
⚠️ void 不等于 undefined。void 表示「不要使用这个值」,把它赋给变量再读取会被拦:
1
2
3
| declare const v: void;
const u: undefined = v; // ✅ 允许
const n: number = v; // ❌ TS2322
|
any 的含义是「放弃类型检查」。它既是所有类型的父类型,也是所有类型的子类型。
1
2
3
4
5
| declare const an: any;
const s: string = an; // ✅ 不报错
const n: number = an; // ✅ 也不报错(同一个值赋给两种类型都行)
an.whatever.deeply.nested(); // ✅ 不报错
const r = an + 1; // r 是 any
|
any 会传染——这是它真正的危险之处:
1
2
3
4
5
| declare const an: any;
const x = an.foo; // x: any
const y = x.bar; // y: any
function f(a: any) { return a.baz; }
const z = f(1); // z: any
|
| 传染途径 | 例子 |
|---|
| 属性访问 | anyValue.anything |
| 函数调用返回值 | anyFn() |
| 数组元素 | anyArr[0] |
| 解构 | const { a } = anyValue |
泛型实参是 any | Promise<any> → await 后仍是 any |
什么时候可以用 any(克制使用):
| 场景 | 是否可接受 |
|---|
| 迁移期临时的类型占位 | 🚧 可以,但必须有 TODO 和期限 |
| 与完全无类型的第三方 JS 互操作 | 🚧 可以,但优先 unknown + 守卫 |
| 写类型体操的内部实现 | 🚧 少见,通常能用泛型替代 |
| 日常业务代码 | 🛑 不要 |
| 想「先让它编译过」 | 🛑 这是在制造技术债 |
💡 打开 noImplicitAny(6.0 随 strict 默认开启)能拦住隐式的 any(如未标注参数),但拦不住你显式写下 any。后者只能靠 code review 和 lint 规则(@typescript-eslint/no-explicit-any)。
unknown 表示「我不知道这是什么类型」。它是 any 的安全替代品:可以接收任何值,但不能直接使用。
1
2
3
4
5
6
7
8
9
10
| function f(u: unknown) {
return u.toFixed(); // ❌ TS18046: 'u' is of type 'unknown'.
}
function g(u: unknown) {
if (typeof u === "number") {
return u.toFixed(2); // ✅ 收窄之后才能用
}
return "not a number";
}
|
any vs unknown 对照:
| 能力 | any | unknown |
|---|
| 接收任意值 | ✅ | ✅ |
| 赋给任意类型 | ✅ | ❌ 需先收窄 |
| 读属性 / 调用方法 | ✅ 不检查 | ❌ TS18046 |
| 会传染 | ✅ 会 | ❌ 不会 |
| 应该用在哪 | 尽量避免 | 外部输入的首选 🔥 |
使用 unknown 的标准姿势:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 所有「外部来的」数据先声明成 unknown,再收窄
async function loadUser(id: string): Promise<User> {
const raw: unknown = await (await fetch(`/api/users/${id}`)).json();
if (!isUser(raw)) { // 自定义守卫,见 11
throw new Error("响应格式不符合预期");
}
return raw; // ✅ 此时已收窄为 User
}
function isUser(v: unknown): v is User {
return typeof v === "object" && v !== null
&& typeof (v as User).name === "string";
}
|
🔥 记住这条规则:凡是从边界进来的数据,类型都是 unknown 而不是 any。any 静默通过,unknown 强迫你处理。详见 11 运行时校验与边界。
never 是空类型——没有任何值属于它。它是所有类型的子类型,可以赋给任何类型;但任何类型(除了 never)都不能赋给它。
1
2
3
4
5
| declare const nv: never;
const s: string = nv; // ✅ never 可以赋给任何类型
const n: number = nv; // ✅ 也可以
const bad: never = 1; // ❌ TS2322: Type '1' is not assignable to type 'never'.
|
never 从哪来:
| 来源 | 例子 |
|---|
| 永远抛异常的函数 | function fail(): never { throw new Error() } |
| 无限循环 | function loop(): never { while (true) {} } |
| 不可能的类型运算 | string & number → never |
| 空联合 | 被 Exclude 过滤光的结果 |
穷尽检查的 default 分支 | 见 03 收窄 🔥 |
用法一:标记永不返回的函数(比 void 更精确)
1
2
3
4
5
6
7
8
9
| function fail(msg: string): never {
throw new Error(msg);
}
// 对控制流分析有意义:TS 知道这行之后不可达
function pick(v: string | undefined): string {
if (v === undefined) fail("缺少 v");
return v; // ✅ TS 知道 v 已不是 undefined,无需再判断
}
|
用法二:穷尽性检查(never 最有价值的用途)
1
2
3
4
5
6
7
8
9
10
11
| type Shape = { kind: "circle"; r: number } | { kind: "square"; s: number };
function area(sh: Shape): number {
switch (sh.kind) {
case "circle": return Math.PI * sh.r ** 2;
case "square": return sh.s ** 2;
default:
const _exhaustive: never = sh; // ✅ 所有分支都覆盖了,sh 已收窄为 never
return _exhaustive;
}
}
|
将来给 Shape 加了新成员,这个 default 会立刻报错:
1
2
3
4
5
6
| type Shape = { kind: "circle"; r: number }
| { kind: "square"; s: number }
| { kind: "triangle"; b: number; h: number }; // 🆕 新增
// ❌ TS2322: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'.
// ← 编译器强迫你处理新情况,而不是运行时静默返回 undefined
|
🔥 这是 TypeScript 最实用的模式之一:让编译器替你记住「加了新变体要改哪些地方」。
这三者名字像,含义差很远。
| 类型 | 接受 | 拒绝 | 说明 |
|---|
{} | 除 null / undefined 外的一切 | null、undefined | 包括 string、number! |
object | 对象、数组、函数、类实例 | 所有原始类型、null、undefined | 想要「非原始」时用它 |
Object | 几乎一切(含原始类型) | null、undefined | 🛑 基本等于没约束,别用 |
1
2
3
4
5
6
7
| const a: {} = "x"; // ✅ 字符串可以!{} 不是「空对象」
const b: object = "x"; // ❌ TS2322: Type 'string' is not assignable to type 'object'.
const c: object = null; // ❌ TS2322: Type 'null' is not assignable to type 'object'.
const d: {} = null; // ❌ TS2322: Type 'null' is not assignable to type '{}'.
const e: object = { x: 1 }; // ✅
const f: object = () => {}; // ✅ 函数也是 object
const g: object = [1, 2]; // ✅ 数组也是 object
|
该用哪个:
| 你想表达 | 写 |
|---|
| 任意非空值 | {}(少见,容易误解) |
| 任意对象(不要原始类型) | object ✅ |
| 任意值 | unknown ✅ |
| 具体形状 | { id: string } 这种 ✅ 最常用 |
⚠️ 想表达「任意对象」时用 Record<string, unknown> 往往比 object 更有用——因为拿到之后还能索引访问。
| 类型 | 能接收 | 能赋给谁 | 能读属性 | 会传染 | 一句话 |
|---|
any | 一切 | 一切 | ✅ | ✅ | 关掉检查 |
unknown | 一切 | 仅 unknown/any | ❌ | ❌ | 安全的 any 🔥 |
never | 仅 never | 一切 | — | — | 空集,表示不可能 |
void | undefined/null | 仅 void/any | ❌ | ❌ | 无返回值 |
{} | 非空一切 | 仅 {}/unknown/any | ❌ | ❌ | 不是空对象 ⚠️ |
object | 非原始 | 仅 object/unknown/any | ❌ | ❌ | 非原始值 |
null / undefined | 自身 | 依赖 strictNullChecks | — | — | 空值 |
赋值关系(谁是谁的子类型):
1
2
3
| never ⊂ 所有类型(never 可赋给一切)
一切 ⊂ unknown(一切可赋给 unknown)
any ↔ 一切(双向自由通行,但不安全)
|
「或」——值属于其中一个即可。
1
2
| type Status = "idle" | "loading" | "done";
type Id = string | number;
|
联合的运算律:
| 表达式 | 结果 | 说明 |
|---|
string | never | string | never 是联合的单位元 |
string | any | any | any 吸收一切 |
string | unknown | unknown | unknown 也吸收一切 |
string | "a" | string | 字面量被父类型吸收 |
string | number | 保留 | — |
⚠️ 使用联合时必须先收窄:
1
2
3
4
5
6
7
8
9
| function f(v: string | number) {
return v.toUpperCase(); // ❌ TS2339: Property 'toUpperCase' does not exist on type
// 'string | number'. Property does not exist on type 'number'.
}
function g(v: string | number) {
if (typeof v === "string") return v.toUpperCase(); // ✅ 已收窄
return v.toFixed(2);
}
|
🔗 收窄的完整手段见 03 收窄与类型守卫。
可辨识联合(discriminated union)——联合最有价值的形态:
1
2
3
| type Result =
| { ok: true; value: string }
| { ok: false; error: Error };
|
关键在于每个成员有一个字面量类型的公共属性(这里是 ok),它让 switch 能精确收窄。这是 TypeScript 里建模「互斥状态」的标准做法。🔥
「且」——同时满足所有成员。
1
2
3
| type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged; // { name: string; age: number }
|
| 表达式 | 结果 | 说明 |
|---|
string & number | never | 不可能同时满足 🔥 |
{ a: 1 } & { b: 2 } | { a: 1; b: 2 } | 属性合并 |
{ a: 1 } & { a: 2 } | { a: never } | 同名属性类型取交叉 ⚠️ |
T & unknown | T | unknown 是交叉的单位元 |
T & any | any | any 吸收一切 |
⚠️ 同名属性的坑:
1
2
3
4
5
6
| type A = { v: string };
type B = { v: number };
type C = A & B; // 类型上合法,但 v 变成 string & number = never
declare const c: C;
c.v; // 类型是 never —— 这个值不可能存在
const bad: C = { v: "x" }; // ❌ TS2322
|
交叉的主要用途:
| 用途 | 例子 |
|---|
| 组合多个「能力」接口 | Loggable & Serializable |
| 品牌类型(模拟名义类型) | string & { readonly __brand: unique symbol } |
| 泛型约束叠加 | <T extends A & B> |
| 混合类的实例类型 | Base & Mixin1 & Mixin2 |
1
2
3
4
5
6
7
8
9
10
11
12
| // 品牌类型:让「字符串」变成不可混淆的「用户 ID」
declare const brand: unique symbol;
type Brand<T, B> = T & { readonly [brand]: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
function toUserId(s: string): UserId { return s as UserId; }
declare const uid: UserId;
declare const oid: OrderId;
const x: OrderId = uid; // ❌ TS2322: 结构不同,无法互赋 🔥
|
💭 品牌类型的取舍:换来编译期的强区分,代价是每个入口都要 as 一次「铸造」。在 ID 满天飞的项目里非常值得,在只有一两种 ID 的项目里是过度设计。
? 可选属性的两种含义(受 exactOptionalPropertyTypes 影响):
1
2
3
4
5
| type Opt = { a?: number }; // a 可以「不存在」,或为 number
const x: Opt = {}; // ✅
const y: Opt = { a: 1 }; // ✅
const z: Opt = { a: undefined }; // 默认允许;开启 exactOptionalPropertyTypes 后 ❌
|
| 开关 | { a: undefined } 能否赋给 { a?: number } |
|---|
exactOptionalPropertyTypes: false(默认) | ✅ 允许 |
exactOptionalPropertyTypes: true | ❌ 报错,两者严格区分 |
readonly 是浅层的 ⚠️:
1
2
3
4
| type R = { readonly a: { b: number } };
declare const r: R;
r.a = { b: 2 }; // ❌ TS2540: Cannot assign to 'a' because it is a read-only property.
r.a.b = 2; // ✅ 合法!readonly 只保护第一层
|
只读数组:
1
2
3
4
5
| let mutable: string[] = ["a"];
let ro: readonly string[] = mutable; // ✅ 可变 -> 只读,允许
let back: string[] = ro; // ❌ TS4104: The type 'readonly string[]' is
// 'readonly' and cannot be assigned to the mutable type 'string[]'.
ro.push("b"); // ❌ TS2339: Property 'push' does not exist on type 'readonly string[]'.
|
变型规则总表(strictFunctionTypes 下实测):
| 结构 | 变型 | 结论 |
|---|
{ readonly a: T } | 协变 | 可变 → 只读 ✅ |
{ a: T } | 不变 | 只读 → 可变 ❌ |
readonly T[] | 协变 | 可变数组 → 只读数组 ✅ |
T[] | 不变 | 只读数组 → 可变数组 ❌ TS4104 |
| 函数参数 | 逆变 | 参数更宽的函数才能赋值 |
| 函数返回值 | 协变 | 返回值更窄的函数才能赋值 |
方法语法 f(x): void | 双变 ⚠️ | 比函数属性宽松,是个历史遗留 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 逆变:参数类型必须更宽
declare const wide: (x: string | number) => void;
declare const narrow: (x: string) => void;
const a: typeof wide = narrow; // ❌ TS2322: 参数 'x' 类型不兼容
const b: typeof narrow = wide; // ✅ 更宽可以赋给更窄
// 方法双变:同样的形状,写成方法就放行了 ⚠️
type FnProp = { f: (x: string | number) => void };
type FnMethod = { f(x: string | number): void };
const p: FnProp = { f: narrow }; // ❌ TS2322
declare const narM: { f(x: string): void };
const m: FnMethod = narM; // ✅ 方法双变,放行
|
⚠️ 数组是协变的,这是一个已知的不健全(unsound)设计:
1
2
3
4
| let strs: string[] = ["a"];
let anys: (string | number)[] = strs; // ✅ 编译器允许
anys.push(123); // ✅ 也允许
strs[1].toUpperCase(); // 💥 运行时崩溃:123.toUpperCase is not a function
|
这是 TypeScript 团队刻意保留的:严格的数组不变性会让日常代码无法忍受。防御手段是优先用 readonly T[] 作为函数参数类型。