这一篇覆盖 TypeScript 日常写代码会用到的全部基础类型:原始类型与字面量、注解与推断、对象类型、数组与元组、函数类型、联合与交叉。它是后面所有章节的地基——类型收窄 讲怎么从联合里挑出一个成员,泛型 讲怎么把类型当参数传递,都建立在这里的语法之上。
类型注解与类型推断
TypeScript 的类型系统有两条路径来知道一个值的类型:
- 类型注解(type annotation):你显式写出来,
value: type - 类型推断(type inference):类型检查器自己算出来
1let count: number; // 注解
2count = 3;
3
4let total = 3; // 推断:number
实用的工作原则只有两条:
- TypeScript 能自动推断出类型时,什么都不用写
- TypeScript 推断不出来时,才需要注解
判断"能不能推断出来"的标准很简单:类型信息是否从值的右侧流到了左侧。有初始化表达式就能推断,没有就不行。
1let a = 3; // 推断 number,不需要注解
2let b; // 推断 any —— 危险
3b = 3;
4b = "text"; // 不报错,因为 b 是 any
let b; 没有初始化表达式,TypeScript 推断不出类型。但它的行为和普通 any 不同,这叫 evolving any(演进式 any):
1let b;
2b = 3;
3b = "text"; // 不报错
4b = true; // 也不报错
实测(--strict):
- 声明后不报
noImplicitAny错误——这是它和函数参数的隐式any的关键区别 - 赋值后类型会收窄到赋进去的类型,可以正常用该类型的方法
- 但可以反复重新赋成不同类型,每次都对
对比函数参数,隐式 any 是直接报错的:
1function f(x) { return x; }
2// error TS7006: Parameter 'x' implicitly has an 'any' type.
它让编译器无法替你发现"同一个变量被当作两种类型用"的问题。所以声明变量时尽量同时初始化,别写 let b;。
strict 是这一系列检查开关的总闸,包含 noImplicitAny、strictNullChecks、strictFunctionTypes 等。新项目一律开:
1{ "compilerOptions": { "strict": true } }
原始类型与字面量类型
TypeScript 的原始类型与 JavaScript 一致,但多了 bigint 和 symbol 的一等支持:
1// 原始类型:注解与推断的对照
2const s = "hello"; // string
3const n = 42; // number
4const b = true; // boolean
5const big = 9007199254740993n; // bigint
6const sym = Symbol("id"); // symbol
7const u = undefined; // undefined
8const nul = null; // null
9
10// 显式注解
11const s2: string = "hello";
12const n2: number = 42;
13
14// 数字分隔符,只是书写形式,值相同
15const million: number = 1_000_000;
16
17// NaN 和 Infinity 都属于 number
18const notANumber: number = NaN;
19const infinity: number = Infinity;
20
21console.log(s, n, b, big, sym.toString(), u, nul);
22console.log(s2, n2, million, notANumber, infinity);
1hello 42 true 9007199254740993 id undefined null
2hello 42 1000000 NaN Infinity
字面量类型
每个原始类型都有一个"只表示一个值"的子类型:
1let a = "hello"; // string
2const c = "hello"; // "hello"
差别在 let 与 const。let 声明的变量可以被重新赋值,所以推断为宽泛的 string;const 声明后不能改,TypeScript 推断出范围最窄的类型,也就是字面量类型 "hello"。
as const 把这个行为扩展到对象和数组:
1// 字面量类型:只表示一个值的类型
2
3let a = "hello"; // 推断为 string(let 可变)
4const c = "hello"; // 推断为 "hello"(const 不可变)
5
6// 显式声明字面量类型
7let direction: "left" | "right" = "left";
8
9// 与 const 断言对比
10const config = { mode: "dark" } as const; // mode 的类型是 "dark",不是 string
11
12// 数字与布尔字面量
13let one: 1 = 1;
14let yes: true = true;
15
16console.log(a, c, direction, config.mode, one, yes);
1hello hello left dark 1 true
config.mode 的类型是 "dark" 而不是 string,因为 as const 让整个对象的所有属性都变成只读的字面量类型。这个特性在 satisfies 与 as const 里展开。
"left" | "right" 这种写法就是字面量类型联合,它是 类型收窄 和可辨识联合的基础。
对象类型
对象类型描述"这个对象有哪些属性"。四种声明方式,按推荐程度排列:
1// 1. 内联对象类型
2function f(p: { x: number; y: number }) {}
3
4// 2. interface(推荐用于对象)
5interface Point { x: number; y: number }
6
7// 3. type 别名
8type Point2 = { x: number; y: number };
9
10// 4. object / Object / {}(都不推荐,见下)
interface 与 type 的选择见 interface 与 type。这里先看对象类型支持的特性:
1interface User {
2 name: string;
3 age: number;
4 email?: string; // 可选属性
5 readonly id: string; // 只读属性
6 [key: string]: unknown; // 索引签名:允许任意其他字符串键
7}
8
9const u: User = {
10 name: "rainboy",
11 age: 18,
12 id: "u_1",
13 extra: true, // 索引签名允许额外键
14};
15
16// u.id = "u_2"; // 错误:只读
17u.name = "rainboylvx";
18
19console.log(u.name, u.age, u.id, u.extra);
1rainboylvx 18 u_1 true
四个要点:
| 特性 | 写法 | 说明 |
|---|---|---|
| 可选属性 | email?: string | 类型实际是 string | undefined |
| 只读属性 | readonly id: string | 只在编译期检查,运行时仍可改 |
| 索引签名 | [key: string]: unknown | 允许任意键,键类型只能是 string/number/symbol |
| 多余属性检查 | 见下 | 对象字面量的特殊规则 |
该用 object、Object 还是 {}
这三个都容易误用:
1let a: object = { x: 1 }; // 任何非原始类型(不含 string/number/boolean/...)
2let b: Object = "text"; // 除 null/undefined 外的所有类型,包括 string!
3let c: {} = 42; // 除 null/undefined 外的所有类型,包括 number!
Object 和 {} 会接受 string、number,几乎从不是你想要的。object 才是"这是个对象",但它不描述结构,访问属性仍然报错。详见 object 与 Object。
多余属性检查
对象字面量赋值给已声明的类型时,TypeScript 会检查有没有多余的键:
1interface Point { x: number; y: number }
2
3// @ts-expect-error 对象字面量的多余属性检查
4const p: Point = { x: 1, y: 2, z: 3 };
但把同一个对象先存到变量,就不再报错:
1const raw = { x: 1, y: 2, z: 3 };
2const p: Point = raw; // 不报错
这是 TypeScript 里最容易让人困惑的规则之一,完整解释见 多余属性检查。
数组与元组
1const nums: number[] = [1, 2, 3];
2const nums2: Array<number> = [4, 5]; // 等价
T[] 和 Array<T> 完全等价,选一种风格保持一致即可。
元组是定长数组,每个位置的类型固定:
1// 数组
2const nums: number[] = [1, 2, 3];
3const nums2: Array<number> = [4, 5]; // 等价写法
4const mixed: (string | number)[] = [1, "a"];
5
6// 元组:定长、位置类型固定
7const pair: [string, number] = ["age", 18];
8
9// 元组可选元素
10const optional: [string, number?][] = [["a"], ["b", 1]];
11
12// 元组剩余元素
13const atLeastOne: [string, ...string[]] = ["a", "b", "c"];
14
15// 只读数组与元组
16const ro: readonly number[] = [1, 2, 3];
17const roTuple: readonly [string, number] = ["a", 1];
18
19// 只读数组不可变
20// @ts-expect-error 只读数组没有 push
21ro.push(4);
22
23console.log(nums, nums2, mixed, pair, optional, atLeastOne);
24console.log(ro, roTuple);
1[ 1, 2, 3 ] [ 4, 5 ] [ 1, 'a' ] [ 'age', 18 ] [ [ 'a' ], [ 'b', 1 ] ] [ 'a', 'b', 'c' ]
2[ 1, 2, 3 ] [ 'a', 1 ]
元组支持三种变形:
1type A = [string, number?]; // 可选元素
2type B = [string, ...string[]]; // 剩余元素(至少一个)
3type C = readonly [string, number]; // 只读元组
元组读起来是"带名字的多个值",但访问靠下标(pair[0]),可读性差。超过 3 个元素、或元素含义不明显时,用对象类型更好。这也是为什么实践中元组主要用于:函数返回多个值、Object.entries 的 [key, value] 这类固定形状。
函数类型
函数的参数必须注解,返回值通常可以推断:
1// 参数与返回值
2function add(a: number, b: number): number {
3 return a + b;
4}
5
6// 可选参数必须在末尾
7function greet(name: string, title?: string): string {
8 return title ? `${title} ${name}` : name;
9}
10
11// 默认参数会自动推断类型
12function repeat(text: string, times = 2): string {
13 return text.repeat(times);
14}
15
16// 剩余参数
17function sum(...nums: number[]): number {
18 return nums.reduce((a, b) => a + b, 0);
19}
20
21// 解构参数需要整体注解
22function dist({ x, y }: { x: number; y: number }): number {
23 return Math.hypot(x, y);
24}
25
26// 函数类型
27type Mapper<T> = (item: T) => string;
28const stringify: Mapper<number> = (n) => String(n);
29
30// 箭头函数返回对象字面量要加括号
31const make = (n: number) => ({ value: n });
32
33// void:不关心返回值
34function log(msg: string): void {
35 console.log(msg);
36}
37
38// never:永不返回
39function fail(msg: string): never {
40 throw new Error(msg);
41}
42
43console.log(add(1, 2), greet("a"), greet("a", "Dr."), repeat("x"), sum(1, 2, 3));
44console.log(dist({ x: 3, y: 4 }), stringify(1), make(1), log("hi"));
45try { fail("boom"); } catch (e) { console.log((e as Error).message); }
13 a Dr. a xx 6
2hi
35 1 { value: 1 } undefined
4boom
几个要点:
可选参数必须在末尾。 function f(a?: number, b: number) 是语法错误。如果确实需要"中间可选",用对象参数:
1function f({ a, b = 1 }: { a: number; b?: number }) {}
默认参数会自动推断类型。 times = 2 让 times 推断为 number,不需要注解。
解构参数需要整体注解。 因为解构本身不提供类型信息:
1function dist({ x, y }: { x: number; y: number }) { }
箭头函数返回对象字面量要加括号,否则 {} 被解析成函数体:
1const make = (n: number) => ({ value: n }); // 括号不能省
void 与 never
| 类型 | 含义 | 例子 |
|---|---|---|
void | 函数不返回值 | function log(msg: string): void |
never | 函数永不正常返回 | 抛异常、死循环 |
never 是所有类型的子类型,可以赋值给任何类型,反之不行。它在 类型收窄 的穷尽性检查里有关键作用。
参数逆变
函数类型的参数是逆变的(contravariant):参数类型更宽的函数可以赋值给参数类型更窄的位置。
1type Handler = (e: MouseEvent) => void;
2
3// 能接受更宽泛的 Event,就可以当 MouseEvent 的处理器用
4const h: Handler = (e: Event) => {};
这个规则由 strictFunctionTypes 控制,strict 下自动开启。它是 strict 家族里最容易被忽视、也最容易在回调场景救你一命的一条。详见 函数类型。
联合与交叉
1type ID = string | number; // 联合:或
2type Person = { name: string } & { age: number }; // 交叉:且
联合类型上的属性访问,只能访问所有成员共有的成员:
1function describe(id: ID): string {
2 return id.toString(); // string 和 number 都有 toString
3 // return id.toUpperCase(); // 错误:number 没有
4}
要用成员特有的属性,必须先收窄,这是下一篇 类型收窄 的主题。
可辨识联合(discriminated union)是最有用的联合模式:给每个成员一个共同的字面量字段作为"标签",然后 switch 那个字段:
1// 联合类型
2type ID = string | number;
3
4// 交叉类型
5type Named = { name: string };
6type Aged = { age: number };
7type Person = Named & Aged;
8
9const p: Person = { name: "rainboy", age: 18 };
10
11// 可辨识联合(判别联合)
12type Shape =
13 | { kind: "circle"; radius: number }
14 | { kind: "square"; side: number }
15 | { kind: "rect"; w: number; h: number };
16
17function area(s: Shape): number {
18 switch (s.kind) {
19 case "circle":
20 return Math.PI * s.radius ** 2;
21 case "square":
22 return s.side ** 2;
23 case "rect":
24 return s.w * s.h;
25 }
26}
27
28// 联合上的属性访问:只有公共成员可用
29function describe(id: ID): string {
30 // 两个类型都有 toString
31 return id.toString();
32}
33
34console.log(p, area({ kind: "circle", radius: 1 }), area({ kind: "rect", w: 2, h: 3 }));
35console.log(describe("a"), describe(1));
1{ name: 'rainboy', age: 18 } 3.141592653589793 6
2a 1
注意 area 函数里没有 default 分支也不报错——TypeScript 检查了所有 kind 都被覆盖。这就是穷尽性检查。
interface 与 type
两者都能描述对象,但能力不同:
1// interface:可扩展、可合并
2interface Animal {
3 name: string;
4}
5interface Animal {
6 age: number; // 声明合并:Animal 现在有 name 和 age
7}
8
9// 继承多个
10interface Pet {
11 owner: string;
12}
13interface Dog extends Animal, Pet {
14 breed: string;
15}
16
17const d: Dog = { name: "wang", age: 3, owner: "rainboy", breed: "corgi" };
18
19// type:可表达联合、元组、条件类型
20type Status = "idle" | "loading" | "done";
21type Pair = [number, number];
22type Maybe<T> = T | null;
23
24// 交叉类型组合对象
25type Employee = Animal & Pet & { salary: number };
26
27const e: Employee = { name: "a", age: 1, owner: "b", salary: 100 };
28
29// interface 无法表达联合
30// interface Status2 = "idle" | "loading"; // 语法错误
31
32// 两者都能描述函数类型
33interface Fn1 { (x: number): string }
34type Fn2 = (x: number) => string;
35
36const f1: Fn1 = String;
37const f2: Fn2 = String;
38
39console.log(d, e, f1(1), f2(2));
1{ name: 'wang', age: 3, owner: 'rainboy', breed: 'corgi' } { name: 'a', age: 1, owner: 'b', salary: 100 } 1 2
关键差异:
| 能力 | interface | type |
|---|---|---|
| 描述对象结构 | ✓ | ✓ |
| 声明合并(同名自动合并) | ✓ | ✗ |
extends 继承 | ✓ | 用 & 交叉 |
| 联合类型 | ✗ | ✓ |
| 元组类型 | ✗ | ✓ |
| 条件类型 | ✗ | ✓ |
| 映射类型 | ✗ | ✓ |
一句话规则:描述对象结构用 interface,需要联合/元组/条件类型时用 type。完整讨论见 interface 与 type。
常见错误速查
以下错误消息都经实际编译验证(@ts-expect-error 锚定,源码见 src/type-basics/errors.ts):
1// 常见错误的正确写法:用 @ts-expect-error 锚定
2// 每行的错误消息都经 tsc 实测
3
4interface Point { x: number; y: number }
5
6// TS2353: 对象字面量的多余属性检查
7// @ts-expect-error z 不在 Point 里
8const p1: Point = { x: 1, y: 2, z: 3 };
9
10// TS2741: 缺少必需属性
11// @ts-expect-error 缺少 y
12const p2: Point = { x: 1 };
13
14// TS2322: 属性类型不匹配
15// @ts-expect-error y 应为 number
16const p3: Point = { x: 1, y: "2" };
17
18interface WithId { readonly id: string }
19const w: WithId = { id: "a" };
20// TS2540: 只读属性不可赋值
21// @ts-expect-error id 是只读的
22w.id = "b";
23
24// TS2339: 访问不存在的属性
25// @ts-expect-error WithId 上没有 nickname
26console.log(w.nickname);
27
28// TS2493: 元组越界
29const t: [string, number] = ["a", 1];
30// @ts-expect-error 长度 2 的元组没有下标 2
31console.log(t[2]);
32
33// TS2345: 数组元素类型不匹配
34const arr: number[] = [1, 2];
35// @ts-expect-error 只能 push number
36arr.push("3");
37
38console.log(p1, p2, p3, t, arr);
| 错误码 | 消息 | 触发场景 |
|---|---|---|
| TS2353 | Object literal may only specify known properties, and 'z' does not exist in type 'Point' | 对象字面量多写了属性 |
| TS2741 | Property 'y' is missing in type '{ x: number; }' but required in type 'Point' | 缺少必需属性 |
| TS2322 | Type 'string' is not assignable to type 'number' | 类型不匹配 |
| TS2540 | Cannot assign to 'id' because it is a read-only property | 给只读属性赋值 |
| TS2339 | Property 'nickname' does not exist on type 'User' | 访问不存在的属性 |
| TS2493 | Tuple type '[string, number]' of length '2' has no element at index '2' | 元组越界 |
| TS2345 | Argument of type 'string' is not assignable to parameter of type 'number' | 实参类型不匹配 |
@ts-expect-error 而不是 @ts-ignore@ts-ignore 会无脑压掉错误,包括将来新出现的错误。@ts-expect-error 要求下一行必须有错误,否则它自己报错。所以在示例代码里用它,既能让 tsc --noEmit 保持干净,又能证明错误确实发生在预期的位置。
小结
| 主题 | 核心结论 |
|---|---|
| 注解 vs 推断 | 有初始化表达式就能推断;let x; 是隐式 any 陷阱 |
| 原始类型 | let 推断宽类型,const 推断字面量类型 |
| 对象类型 | 用 interface/type,别用 Object/{} |
| 元组 | 定长、位置类型固定,超过 3 个元素考虑换对象 |
| 函数 | 参数必须注解,可选参数在末尾,参数逆变 |
| 联合 | 只能访问公共成员,要收窄 |
| 可辨识联合 | 共同字面量字段 + switch = 穷尽性检查 |
下一篇:类型收窄。