类型基础

2026-09-09 00:00    #typescript  

这一篇覆盖 TypeScript 日常写代码会用到的全部基础类型:原始类型与字面量、注解与推断、对象类型、数组与元组、函数类型、联合与交叉。它是后面所有章节的地基——类型收窄 讲怎么从联合里挑出一个成员,泛型 讲怎么把类型当参数传递,都建立在这里的语法之上。

类型注解与类型推断

TypeScript 的类型系统有两条路径来知道一个值的类型:

1let count: number;   // 注解
2count = 3;
3
4let total = 3;       // 推断:number

实用的工作原则只有两条:

  1. TypeScript 能自动推断出类型时,什么都不用写
  2. 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):

对比函数参数,隐式 any 是直接报错的:

1function f(x) { return x; }
2// error TS7006: Parameter 'x' implicitly has an 'any' type.
evolving any 仍然是隐患

它让编译器无法替你发现"同一个变量被当作两种类型用"的问题。所以声明变量时尽量同时初始化,别写 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

关键差异:

能力interfacetype
描述对象结构✓✓
声明合并(同名自动合并)✓✗
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);
错误码消息触发场景
TS2353Object literal may only specify known properties, and 'z' does not exist in type 'Point'对象字面量多写了属性
TS2741Property 'y' is missing in type '{ x: number; }' but required in type 'Point'缺少必需属性
TS2322Type 'string' is not assignable to type 'number'类型不匹配
TS2540Cannot assign to 'id' because it is a read-only property给只读属性赋值
TS2339Property 'nickname' does not exist on type 'User'访问不存在的属性
TS2493Tuple type '[string, number]' of length '2' has no element at index '2'元组越界
TS2345Argument 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 = 穷尽性检查

下一篇:类型收窄。