基本类型
这一课你将能做到
- 给变量正确标注
string / number / boolean / 数组 / 元组 / 枚举
- 说清楚
any / unknown / never / void 四个特殊类型各自的用途和使用时机
- 理解字面量类型与类型推断,知道什么时候该写注解、什么时候让 TS 自己推
为什么要学
类型系统是 TypeScript 的全部地基,而基本类型是地基里的第一层砖。后面要学的接口、泛型、工具类型,全都是在这些基础类型之上做组合。
更重要的是避坑:初学者最容易栽在三个地方——把 any 当万能钥匙、分不清 null/undefined 和 never、不知道数组的 number[] 和 Array<number> 是同一个东西。这一课一次性讲透。
先建立图像
TS 基本类型的家族树:
原始类型:string number boolean null undefined symbol bigint
特殊类型:any unknown never void
复合类型:数组 T[] 元组 [T1, T2] 枚举 enum 字面量类型 "a" | 1 | true
对象类型:object / interface / type(下一课详讲)
记忆口诀:
any = 放弃治疗(什么都能装,装完随便用)
unknown = 安全的不确定(什么都能装,用之前必须验明正身)
never = 永远不可能(函数永远回不来、穷尽检查兜底)
void = 没返回值(函数只干活不交货)
跟着做
新建 types-demo.ts,所有代码都可以直接 tsc types-demo.ts && node types-demo.js 运行。
步骤 1:三种原始类型
let username: string = "小明";
let age: number = 25;
let isMember: boolean = true;
// 模板字符串也是 string
let intro: string = `${username} 今年 ${age} 岁`;
console.log(intro); // 小明 今年 25 岁
注意:TS 里 number 不区分整数浮点数,25 和 25.5 都是 number。
步骤 2:类型推断——大多数时候不用写注解
let city = "北京"; // TS 自动推断为 string
let score = 99.5; // 自动推断为 number
// city = 123; // 报错:Type 'number' is not assignable to type 'string'
原则:变量初始化时 TS 能推出来的,就别手写。注解主要用在「推不出来」的地方:函数参数、空数组、初始为 null 的变量。
步骤 3:数组与元组
// 数组:所有元素同类型
let tags: string[] = ["前端", "TS", "React"];
let scores: number[] = [90, 85, 77];
let flags: Array<boolean> = [true, false]; // 泛型写法,等价于 boolean[]
tags.push("全栈");
// tags.push(1); // 报错
// 元组:固定长度、每个位置类型固定
let user: [string, number] = ["小明", 25];
console.log(user[0].toUpperCase()); // 第一个位置是 string,有 string 的方法
// let bad: [string, number] = [25, "小明"]; // 报错:顺序错了
数组 vs 元组:数组是「任意长度、同一类型」,元组是「固定长度、各位置类型可不同」。React 的 useState 返回值就是元组:[状态, 设置函数]。
步骤 4:枚举 enum
enum Status {
Draft, // 0
Published, // 1
Archived, // 2
}
let articleStatus: Status = Status.Published;
console.log(articleStatus); // 1
console.log(Status[1]); // "Published"(反向映射)
// 字符串枚举(实际项目更常用,日志可读)
enum Role {
Admin = "ADMIN",
Editor = "EDITOR",
Viewer = "VIEWER",
}
let myRole: Role = Role.Admin;
什么时候用枚举:一组固定的、穷尽的取值,比如订单状态、用户角色。如果取值是开放的字符串,用字面量联合(后面讲)更合适。
步骤 5:any、unknown、never、void 四连
// any:放弃检查(能不用就不用)
let flexible: any = "hello";
flexible = 123;
flexible.foo.bar; // 编译不报错,运行时炸
// unknown:安全的不确定
let input: unknown = JSON.parse('"hello"');
// input.toUpperCase(); // 报错:必须先收窄
if (typeof input === "string") {
console.log(input.toUpperCase()); // OK:HELLO
}
// void:函数没有返回值
function logMessage(msg: string): void {
console.log(msg);
// 没有 return,或 return; 空返回
}
// never:永远不会有结果
function throwError(message: string): never {
throw new Error(message); // 抛异常,函数永远走不到结尾
}
function infiniteLoop(): never {
while (true) {} // 死循环,也回不来
}
步骤 6:null、undefined 与字面量类型
// strictNullChecks 开启时(strict 模式默认开)
let name: string = "张三";
// name = null; // 报错!string 不包含 null
let nickname: string | null = null; // 显式允许 null
nickname = "小张";
// 字面量类型:值本身就是类型
let direction: "up" | "down" | "left" | "right";
direction = "up";
// direction = "forward"; // 报错:不在字面量列表里
let version: 1 | 2 | 3 = 2;
| null 和字面量联合是日常写得最多的类型组合,务必练熟。
步骤 7:完整可运行示例
enum Level {
Beginner = "BEGINNER",
Advanced = "ADVANCED",
}
function formatStudent(name: string, age: number, level: Level): string {
const tag: string = level === Level.Beginner ? "初学者" : "进阶者";
return `${name}(${age} 岁,${tag})`;
}
const students: string[] = [
formatStudent("小明", 20, Level.Beginner),
formatStudent("小红", 28, Level.Advanced),
];
for (const s of students) {
console.log(s);
}
// 输出:
// 小明(20 岁,初学者)
// 小红(28 岁,进阶者)
讲清楚要点
- 优先靠推断,其次写注解:
let x = 1 不用写 : number。注解的主战场是函数签名和空值初始化。
unknown 是 any 的安全替代:任何值都能赋给 unknown,但使用它之前必须用 typeof 等手段收窄,编译器强制你验货。
never 表示「不可能到达」:抛出异常的函数、死循环函数返回 never;它还用于联合类型的穷尽检查(后面类型收窄课详讲)。
void vs undefined:void 是函数返回类型,表示「不关心返回值」;undefined 是一个具体的值。写函数返回类型用 void。
- 元组是定长异构数组:
[string, number] 长度必须为 2,位置类型固定。解构赋值时配合极佳。
- strict 模式下 null 不再隐身:
string 类型装不下 null,必须写成 string | null,这能拦住线上最多的「读 null 的属性」事故。
你可能会懵的地方
-
number[] 和 Array<number> 有区别吗?
没有,完全等价。短类型用 number[],复杂嵌套如 Array<string | number> 用泛型写法更清晰。
-
枚举编译后是什么样?
数字枚举会编译成一个带双向映射的对象(Status[0] = "Draft" 且 Status["Draft"] = 0)。字符串枚举只有正向映射。担心体积可以用 const enum 或字面量联合替代。
-
为什么 let a = null 推断出来不是 null 类型?
strict 模式下,let a = null 推断为 any(历史包袱),const a = null 才推断为字面量 null。所以初始为空的变量要手写注解:let a: string | null = null。
-
元组能用 push 加元素吗?
类型层面不允许越界,但 push 一个符合位置类型的元素编译器有时放行——这是 TS 已知的局限性,靠团队约定别这么干。
-
object 类型有什么用?
表示「非原始类型」,但太宽泛(数组、函数、对象都算),实际开发中几乎不用,都是用 interface 精确描述形状。
错法对照
| 错法 | 问题 | 正解 |
|---|
拿不准就标 any | 类型检查失效,错误延迟到运行时 | 用 unknown + 收窄,或定义精确类型 |
function f(): undefined | undefined 不是「无返回」的写法 | 无返回写 void |
let arr = [] 之后 arr.push(1) | 空数组推断为 any[](隐式 any) | 初始化时标注:let arr: number[] = [] |
用魔法数字判断状态 if (s === 1) | 可读性差、易写错 | 用枚举 Status.Published 或字面量联合 |
let x: string = null | strict 模式报错 | 需要可空就写 string | null |
卡点急救
- 报错
Type 'null' is not assignable to type 'string':strict 模式在工作。要么改类型为 string | null,要么赋值前判空。
- 空数组 push 报错
implicitly has type 'any[]':声明时给类型:const list: string[] = []。
- 枚举值打印出来是数字:数字枚举本来就这样,日志要可读请改用字符串枚举。
- JSON.parse 结果是
any:TS 4.x 起 JSON.parse 返回 any,手动转 unknown 再收窄,避免类型污染扩散。
面试常问
-
any 和 unknown 的区别?
赋值层面一样(什么都能装),使用层面相反:any 可以任意操作不报错;unknown 必须先类型收窄才能操作。unknown 是类型安全的 any。
-
never 类型有什么实际用途?
两个:一是永远抛错或死循环的函数返回值;二是 switch 穷尽检查——把 never 赋给未处理的联合分支,新增类型时编译器会提醒你补 case。
-
null 和 undefined 在 TS 里的区别?
undefined 表示「声明了没赋值 / 参数没传 / 属性不存在」;null 表示「显式的空」。strict 模式下两者都是独立类型,不能随意赋给其他类型。TS 官方风格指南建议统一用 undefined。
-
什么时候用元组而不是数组?
当结构固定、位置有意义时:如坐标 [number, number]、键值对 [string, number]、React useState 的返回值。元素同构且数量不定就用数组。
自测三问
function f(): never 和 function f(): void 分别描述什么样的函数?
let a = [] 在 strict 模式下有什么问题?怎么写才对?
- 后端返回的 JSON 数据应该标成
any 还是 unknown?为什么?
小结一句话
基本类型是 TS 的字母表:原始类型管值,数组元组管集合,枚举管固定取值,any/unknown/never/void 管四种特殊情况——用对了,类型系统才开始为你干活。
延伸