基础类型注解
给变量、数组、对象标注类型,让编译器帮你把关
🎯 学习目标
学完本章,你将能够:
- 能够为变量、常量添加 number、string、boolean 等基础类型注解
- 能够理解并运用 unknown、any、never、void 等特殊类型
- 能够掌握类型推断规则,在合适时机选择显式标注
- 能够使用类型断言(as、<>)在确定场景下收窄类型
- 能够理解元组、字面量类型与 const 断言的使用场景
怎么告诉 TS
这个变量是数字,那个是字符串,别搞混了
TypeScript 的类型注解语法很简单:在变量名后面加 : 类型。
// JS 写法:没有类型
let name = "张三";
// TS 写法:加上类型注解
let name: string = "张三";
这一章我们来学习所有基础类型的注解方式。
Part 1: 原始类型
string、number、boolean——和 JS 完全对应,只是多了类型注解
TypeScript 的类型注解语法很简单:在变量名后面加 : 类型。这一节从最基础的三个类型开始——string、number、boolean,它们和 JS 完全对应,只是多了一层编译时检查。同时你还会学到什么时候该手动标注,什么时候让 TS 自己推断。
三大原始类型
字符串、数字、布尔值的类型注解
// 字符串
let name: string = "张三";
name = "李四"; // OK
// name = 42; // 报错:不能将 number 赋值给 string
// 数字
let age: number = 25;
let hex: number = 0xff; // 十六进制
let binary: number = 0b1010; // 二进制
let float: number = 3.14;
// 布尔
let isActive: boolean = true;
let hasPermission: boolean = false;
用人话说:类型注解就像给变量贴标签——"这个盒子只能装字符串"。你往里塞数字,编译器就会提醒你贴错了标签。
更多原始类型
bigint、symbol 以及类型推断
// bigint(ES2020)
let big: bigint = 9007199254740993n;
// symbol
let sym: symbol = Symbol("unique");
// 类型注解的两种方式
let x: number = 42; // 显式注解
let y = 42; // 类型推断,等同于 let y: number = 42
Part 2: 数组类型
数组的元素类型也可以标注,防止混入错误类型的数据
原始类型的注解掌握了,但实际开发中你用得最多的可能是数组。JS 数组什么都能往里塞,数字、字符串混着来也不报错。TypeScript 可以给数组标注元素类型,确保 number[] 里不会冒出字符串。这一节还会学到只读数组、联合类型数组,以及 as const 怎么把数组"冻住"。
数组注解方式
T[] 和 Array<T> 两种写法等价
// JS 写法:没有类型约束
let nums = [1, 2, 3];
nums.push("hello"); // 不会报错
// TS 写法1:T[](推荐)
let nums: number[] = [1, 2, 3];
// nums.push("hello"); // 报错!只能 push 数字
// TS 写法2:Array
let names: Array<string> = ["张三", "李四"];
// 多维数组
let matrix: number[][] = [[1, 2], [3, 4]];
// 只读数组
let readonlyArr: readonly number[] = [1, 2, 3];
// readonlyArr.push(4); // 报错
// readonlyArr[0] = 99; // 报错
联合与不可变
数组元素可以是多种类型,也可以锁定为不可变
// 联合类型数组:元素可以是 string 或 number
let mixed: (string | number)[] = [1, "hello", 2, "world"];
// 使用 const 断言创建不可变数组
const COLORS = ["red", "green", "blue"] as const;
// 类型为 readonly ["red", "green", "blue"]
// COLORS.push("yellow"); // 报错
// COLORS[0] = "pink"; // 报错
Part 3: 空值处理
null 和 undefined 是最常见的错误来源,TypeScript 有专门的机制来处理它们
Cannot read property of null——你一定见过这个报错。null 和 undefined 是 JS 里最狡猾的"地雷",踩到就炸。TypeScript 开启 strictNullChecks 后,null 不能随便赋值给其他类型,编译器逼你先检查再使用。这一节学会用可选链 ?. 和空值合并 ?? 安全地处理空值。
严格空值检查
开启 strict 后,null 不能随便赋值
// strictNullChecks 开启后
// JS 写法:不会报错,运行时可能崩溃
// let name = null;
// name.toUpperCase(); // Cannot read property of null
// TS 写法:编译时就拦截
// let name: string = null; // 报错!
// let age: number = undefined; // 报错!
// 正确做法:使用联合类型
let name: string | null = null;
let age: number | undefined = undefined;
// 可选属性自动添加 | undefined
interface User {
name: string;
email?: string; // 等同于 email: string | undefined
}
// 可选参数也一样
function greet(name?: string) {
// name 的类型是 string | undefined
console.log(name?.toUpperCase());
}
非空断言
告诉 TS "我确定这个值不是 null"
// 非空断言操作符 !
let input = document.getElementById("app")!;
// 告诉 TS:我确定这个元素存在,别报错
// 可选链与空值合并配合使用
let user: { name: string | null } = { name: null };
let displayName = user.name ?? "匿名"; // "匿名"
建议:始终开启 strictNullChecks。虽然需要多写一些 | null,但能避免大量运行时的 Cannot read property of null 错误。
☕ 学习断点
你已经完成了前半部分,休息一下吧
前半部分我们学习了原始类型、数组类型、空值处理和字面量类型——这些都是 TS 类型系统的基础。
建议:休息 5 分钟,回顾一下学习目标,确认都达成了再继续。
💡 后半部分将学习 any/unknown、类型推断、never 和类型断言——这些是写出类型安全代码的关键。
Part 4: 字面量类型
变量只能取特定的值——比枚举更轻量
有些变量不该接受任意字符串,比如 HTTP 方法只能是 "GET" 或 "POST"。字面量类型就是干这个的:把变量的取值范围限定在你指定的几个值里。比枚举更轻量,写法也更简单。这一节看完你就知道怎么用字面量类型做"穷举式"的参数校验。
限定取值范围
变量只能是你指定的那几个值
// 字符串字面量
let direction: "left" | "right";
direction = "left"; // OK
// direction = "up"; // 报错
// 数字字面量
let statusCode: 200 | 404 | 500;
statusCode = 200; // OK
// statusCode = 201; // 报错
// 实际应用:限定参数值
type Method = "GET" | "POST" | "PUT" | "DELETE";
function request(url: string, method: Method) {
// method 只能是以上四个值之一
}
request("/api/users", "GET"); // OK
// request("/api/users", "FETCH"); // 报错
// 结合 as const 实现字面量推断
const config = {
env: "development" as const, // 类型为 "development" 而非 string
port: 3000 as const, // 类型为 3000 而非 number
};
Part 5: any 与 unknown
当你不知道类型时,有两种选择——一种安全,一种不安全
不是所有值的类型都能提前确定。第三方 API 返回的数据、动态解析的 JSON,类型都是"未知"的。这时候有两个选择:any 关闭检查,unknown 要求先检查再使用。这一节对比两者的区别,告诉你为什么 any 是"万恶之源",以及 unknown 怎么安全替代它。
any:关闭检查
万恶之源,能不用就不用
// any —— 关闭类型检查
let anything: any = "hello";
anything = 42; // OK
anything = {}; // OK
anything.foo(); // 编译不报错,运行时崩溃
anything.bar.baz; // 编译不报错,运行时崩溃
unknown:安全替代
类型安全的 any,使用前必须先检查
// unknown —— 类型安全的 any
let unsure: unknown = "hello";
unsure = 42; // OK——可以赋任何值
// 但使用前必须收窄(narrow)
// unsure.toUpperCase(); // 报错!不能直接使用
// 正确用法1:typeof 检查
if (typeof unsure === "string") {
console.log(unsure.toUpperCase()); // OK
}
// 正确用法2:类型断言
console.log((unsure as string).toUpperCase());
// 正确用法3:类型守卫函数
function isString(val: unknown): val is string {
return typeof val === "string";
}
三者对比
any vs unknown vs never
| 类型 | 含义 | 安全性 | 使用场景 |
|---|---|---|---|
any | 任何值,跳过检查 | 无 | 迁移旧代码、快速原型 |
unknown | 任何值,使用前须收窄 | 高 | 第三方 API 响应、泛型 |
never | 不可能存在的值 | - | 穷尽检查、不可能的分支 |
Part 6: 类型推断
很多时候不需要手动标注,TS 会自动推断类型
写了这么多类型注解,你可能会问:每个变量都要手动标注吗?不用。TypeScript 很聪明,你写 let x = 42,它就知道 x 是 number。但推断不是万能的——函数参数就不会推断,对象和数组有时推断得不够精确。这一节搞清楚什么时候该让 TS 推断,什么时候必须手动标注。
推断规则
能推断就让 TS 推断,减少冗余代码
// 变量初始化自动推断
let x = 42; // 推断为 number
let s = "hello"; // 推断为 string
let b = true; // 推断为 boolean
let arr = [1, 2]; // 推断为 number[]
// 对象推断
let user = {
name: "张三", // string
age: 25, // number
};
// user 的类型: { name: string; age: number }
// 函数返回值推断
function add(a: number, b: number) {
return a + b; // 自动推断返回 number
}
// 函数参数不会自动推断(需要手动标注)
// function greet(name) { } // 报错:参数隐式有 any 类型
function greet(name: string) { } // 正确
何时手动标注
四种必须手动标注类型的情况
函数参数
参数类型不会自动推断,必须手动标注。
更宽泛的类型
let arr: (string|number)[] = [] 需要显式声明。
返回值不明确
复杂函数的返回值类型最好标注清楚。
const 断言
as const 让推断更精确,锁定字面量类型。
原则:能推断就让 TS 推断。函数参数必须标注,返回值通常可以推断。
元组类型
长度固定、每个位置类型也固定的数组
普通数组所有元素类型相同,但有时你需要"第0个是字符串,第1个是数字"这种精确控制。元组就是干这个的。
// 普通数组:所有元素类型相同
let names: string[] = ["张三", "李四"];
// 元组:每个位置的类型固定
let userTuple: [string, number] = ["张三", 25];
// 类型↑ 类型↑
// userTuple[0] 是 string,userTuple[1] 是 number
// 越界或类型不对都会报错
// userTuple = [25, "张三"]; // 报错!顺序反了
// userTuple = ["张三"]; // 报错!缺少第二个元素
// 实际应用场景:函数返回多个值
function useCoords(): [number, number] {
return [39.9, 116.4]; // 经纬度
}
const [lat, lng] = useCoords();
// lat 和 lng 自动推断为 number
// RGB 颜色值
let color: [number, number, number] = [255, 128, 0];
元组在 React 的 useState 返回值中就用到了:[state, setState] 就是一个元组。虽然你可能没注意到,但你已经在用元组了。
类型拓宽
为什么 let 和 const 推断的结果不一样?
看看这个"诡异"的现象:
// let 会"拓宽"类型
let age = 25; // 推断为 number(不是 25)
age = 30; // OK——因为类型是 number
// const 会"收窄"为字面量
const AGE = 25; // 推断为 25(字面量类型)
// AGE = 30; // 报错!25 不能赋值为 30
// 对象和数组的拓宽更隐蔽
let obj = { x: 1 }; // { x: number }
const obj2 = { x: 1 }; // 还是 { x: number }!
// const 只锁定了变量本身,对象内部还是会拓宽
// 解决方案:as const
const obj3 = { x: 1 } as const; // { readonly x: 1 }
// obj3.x = 2; // 报错!
let arr = [1, 2, 3]; // number[]
const arr2 = [1, 2, 3] as const; // readonly [1, 2, 3]
let 拓宽为宽泛类型,const 收窄为字面量。但 const 对象和数组的内部属性仍然会被拓宽,需要 as const 才能锁定。

图:类型拓宽的三种路径对比
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 三行流程:const AGE=25→字面量25(绿色实线框"冻结") | let age=25→number(橙色虚线框"拓宽") | let age=25 as const→字面量25(绿色实线框"手动冻结") - 右侧箭头说明:const和as const产生精确类型,let产生宽松类型 - 配色:浅灰背景,绿色=精确类型,橙色=拓宽类型,16:9
类型层次
从 any 到 never,TS 类型的完整谱系

图:TypeScript 类型系统层次结构
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:从上到下的金字塔/层次结构 - 顶部:any(接受一切,红色警告标注"危险") - 第二层:unknown(安全接受一切,绿色标注"安全") - 第三层:具体类型(string、number、boolean、object、数组等,蓝色系) - 底部:never(不可能的值,灰色标注"不可达") - 侧面标注 void 和 null/undefined 的位置 - 配色建议:浅灰背景,从上到下由暖色渐变到冷色,各类型用圆角矩形 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
// 类型谱系:从"什么都能装"到"什么都不能装"
// any → 关闭检查,什么都能装(危险)
// unknown → 什么都能装,但用之前必须检查(安全)
// string / number / boolean / object → 具体类型
// "hello" / 42 / true → 字面量类型(最精确)
// never → 不可能的值,什么都不能装
| 类型 | 能赋值给它 | 能赋值给别人 | 一句话总结 |
|---|---|---|---|
any | 任何类型 | 任何类型 | 逃出类型系统的后门 |
unknown | 任何类型 | 收窄后才行 | 安全的"不确定" |
string | string | string 超集 | 具体类型 |
"hello" | 仅 "hello" | string | 字面量,最精确 |
never | never(不可能) | 任何类型 | 不可能存在的值 |
Part 7: never 详解
never 表示永远不可能存在的值
前面的对比表中提到过 never,但没细说。它是 TS 类型系统中最"极端"的类型——表示永远不可能存在的值。抛异常的函数返回 never,不可能的交叉类型结果是 never,switch 的 default 分支也能用 never 做穷尽检查。这一节深入理解 never 的三大出现场景和它的实战用途。
never 是什么
不可能存在的值的类型
never 不会出现在你的变量声明里,但会在以下场景自动出现:
// 场景 1:不可能的交叉类型
type Impossible = string & number;
// 类型是 never——一个值不可能同时是 string 和 number
// 场景 2:永远不会返回的函数
function fail(msg: string): never {
throw new Error(msg); // 抛出异常,函数不会正常返回
}
function infiniteLoop(): never {
while (true) {} // 死循环,永远不会返回
}
// 场景 3:收窄到不可能的分支
function check(x: string | number) {
if (typeof x === "string") {
// x 是 string
} else if (typeof x === "number") {
// x 是 number
} else {
// x 的类型是 never——这个分支永远走不到
console.log(x); // never
}
}
可以这么理解:never 是类型系统的"底线"——如果一个变量被推断为 never,说明逻辑上这个分支不可能到达,代码有问题或者只是防御性写法。
never 详解(上)
never 出现的三大场景与 void 区别
// 场景 1:不可能的联合交叉
type StrAndNum = string & number; // never
type Both = "left" & "right"; // never
// 场景 2:类型收窄后的死代码分支
function assert(x: string | number) {
if (typeof x === "string") {
x; // string
} else if (typeof x === "number") {
x; // number
} else {
x; // never —— 这个分支永远不会执行
}
}
// 场景 3:never 在联合类型中会被自动过滤
type Mixed = string | never | number;
// 等价于 string | number,never 被吞掉了!
type Filtered = Extract;
// Extract 排除后,剩下的还是 number
void 表示"我选择不返回值"(函数正常结束),never 表示"这个函数根本回不来"(抛异常或死循环)。void 是合法的返回类型,never 是"不可能到达终点"的类型。
never 穷尽检查
让 switch 语句永远不会漏掉分支
// 穷尽检查(Exhaustive Check)的经典写法
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.size ** 2;
default:
// 如果 Shape 新增了类型,这里会报错!
const _exhaustive: never = shape;
throw new Error(`未知图形: ${_exhaustive}`);
}
}
// 更通用的 assertNever 工具函数
function assertNever(x: never): never {
throw new Error("不可能到达的分支: " + x);
}
// 使用方式:
type Status = "loading" | "success" | "error";
function handleStatus(status: Status) {
switch (status) {
case "loading": return "加载中...";
case "success": return "成功";
case "error": return "出错了";
default: return assertNever(status);
// 如果 Status 新增了值,这里会报编译错误
}
}
Shape 或 Status 新增一个变体,却忘了在 switch 里处理,TS 会在 default 分支报错。这就是 never 的威力——让遗漏的分支无所遁形。

图:never 穷尽检查的工作原理
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 左侧:联合类型 Shape = Circle | Square | Triangle 三个彩色方块 - 中间:switch流程,每个case用箭头指向处理逻辑 - 右侧:default分支接收never(灰色),标注"所有类型已被处理" - 下方:添加新类型后,default不再是never→编译错误(红色感叹号) - 关键标注:"never=不可能出现的值"、"添加新成员→编译器帮你找到遗漏" - 配色:浅蓝背景,Shape用不同颜色,错误红色,箭头深灰,16:9
never 详解(下)
assertNever 模式与穷尽 switch 实战
// 经典 exhaustive switch 模式
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle": return Math.PI * shape.radius ** 2;
case "square": return shape.size ** 2;
default:
const _exhaustive: never = shape;
throw new Error(`Unknown: ${_exhaustive}`);
}
}
// 如果新增 triangle 但忘加 case:
// type Shape = ... | { kind: "triangle"; base: number };
// default 分支的 _exhaustive 就不再是 never
// TS 会报错:不能将 Shape 赋值给 never → 你立刻发现漏了!
// 实际项目中的类型保护
type ApiResponse =
| { status: "success"; data: string }
| { status: "error"; message: string }
| { status: "loading" };
function handle(res: ApiResponse) {
switch (res.status) {
case "success": return res.data;
case "error": throw new Error(res.message);
case "loading": return "请稍候...";
default: return assertNever(res);
}
}
穷尽检查的核心逻辑:在 default 分支把值赋给 never 类型的变量。如果所有分支都处理了,值在 default 里一定是 never,赋值合法;如果漏了分支,值的类型就不是 never,编译立即报错。
never 对比 void
返回值类型中两个容易混淆的类型
| 类型 | 含义 | 函数能否正常返回 | 使用场景 |
|---|---|---|---|
void | 没有有意义的返回值 | 能,返回 undefined | 不关心返回值的函数 |
never | 永远不会返回 | 不能,要么抛异常要么死循环 | 抛错函数、穷尽检查 |
// void:函数正常执行完毕,只是没有返回值
function log(msg: string): void {
console.log(msg);
// 函数到这里正常结束,隐式返回 undefined
}
const result = log("hello"); // result 是 void(实际值是 undefined)
// never:函数根本不会正常结束
function throwError(msg: string): never {
throw new Error(msg);
// 后面的代码永远执行不到
}
// 区别在于:void 是"我选择不返回",never 是"我根本回不来"
简单记:void = 函数能跑完但没返回值;never = 函数跑不完(抛错或死循环)。
Part 8: 类型断言
有时候你比 TS 更了解一个值的类型,类型断言就是告诉编译器"相信我"
document.getElementById 返回 HTMLElement | null,但你确定页面上有这个元素。每次都要判空太烦了,能不能直接告诉 TS"它不是 null"?类型断言就是干这个的——as HTMLInputElement 一写,TS 就信你了。但滥用断言等于给自己挖坑,这一节学会正确使用断言和 as const。
断言语法与场景
两种写法、三种常见场景
// 语法 1:as Type(推荐,TSX 中也能用)
let val: unknown = "hello";
let len: number = (val as string).length;
// 语法 2:(JSX/TSX 中不能用)
let len2: number = (<string>val).length;
// 场景 1:DOM API(最常见)
const input = document.getElementById("email") as HTMLInputElement;
// 没有 as,TS 认为它可能是 null,无法访问 .value
console.log(input.value);
// 场景 2:收窄联合类型
function process(value: string | number) {
const str = value as string; // 你确定这里是 string
console.log(str.toUpperCase());
}
// 场景 3:从 any 迁移
interface LegacyData {
name: string;
}
const raw: any = { name: "张三" };
const data = raw as LegacyData; // 逐步引入类型
断言陷阱与 const
不要滥用断言,善用 as const
// ❌ 错误用法:强制不兼容的类型
// let x: string = "hello";
// let n: number = x as number; // 报错!string 不能断言为 number
// 双重断言:终极逃生舱(极度危险)
let n = x as unknown as number; // 先转为 unknown,再转为 number
// 能编译通过,但运行时 n 还是字符串——隐患极大!
// as const:让类型锁定为字面量
const req = { method: "GET", url: "/api/users" } as const;
// 类型变为 { readonly method: "GET"; readonly url: "/api/users" }
// 而不是 { method: string; url: string }
// 实际用处:函数参数需要字面量类型
function request(url: string, method: "GET" | "POST") {}
const options = { url: "/api", method: "GET" } as const;
request(options.url, options.method); // OK!
// 没有 as const,options.method 是 string,调用会报错
断言 vs 类型守卫:能用 if (typeof x === "string") 收窄就不用断言。类型守卫是安全的(运行时真检查了),断言只是你的一厢情愿。只有当你确实比 TS 更了解类型时才用断言。
断言深入解析
as const 全场景与双重断言的危险
// as const 三种用法
// 1. 整个对象/数组 as const
const config = { host: "localhost", port: 3000 } as const;
// { readonly host: "localhost"; readonly port: 3000 }
// 2. 单个属性 as const
const mixed = { name: "app", version: 1 as const };
// { name: string; version: 1 } — 只有 version 被锁定
// 3. 非 const 上下文中的变量
let x = "hello" as const; // x 的类型是 "hello" 而非 string
// 双重断言(as unknown as X)——万不得已才用
interface ApiRes { code: number; data: unknown; }
const raw = '{"code":200,"data":{"name":"张三"}}';
const res = JSON.parse(raw) as unknown as ApiRes;
// JSON.parse 返回 any,先用 as unknown 再转具体类型
// 实际上应该用类型守卫或校验库(如 zod)代替
// 尖括号语法在 TSX 中不能用
// value ← TSX 里被当成 JSX 标签
// value as string ← 用这种写法就对了
typeof、instanceof)或类型注解替代,就不要用断言。双重断言 as unknown as X 几乎总是代码异味,意味着类型设计有问题。
void 返回类型
void、undefined 与 never 在函数中的区别
// void:函数不关心返回值
function log(msg: string): void {
console.log(msg);
}
// undefined:函数必须显式返回 undefined
function getNothing(): undefined {
console.log("nothing");
return undefined; // 必须写 return
}
// never:函数永远回不来
function crash(): never {
throw new Error("boom");
}
// void 在回调中的特殊行为
type Callback = (data: string) => void;
// 回调函数返回了值也没关系!
const cb: Callback = (data) => {
console.log(data);
return 42; // 不报错!void 回调忽略返回值
};
// 这就是为什么 Array.forEach 的回调可以 return
[1, 2, 3].forEach((n) => {
return n * 2; // forEach 期望 () => void,忽略返回值
});
当函数类型声明返回 void 时,实际传入的函数可以返回任意值——这个返回值会被安全忽略。这是 TS 故意设计的,让回调函数写法更灵活。
void 深入理解
void 回调设计原理与 undefined 的区别
// void 回调的设计意图
type Fn = () => string;
type VoidFn = () => void;
const f1: Fn = () => "hello"; // 返回值必须是 string
const f2: VoidFn = () => "hello"; // 返回值可以是任意类型!
// VoidFn 的实现返回什么都可以,调用方拿到的返回值始终是 void
// Array.prototype.forEach 的签名
// forEach(callback: (element: T) => void): void
// 所以回调里 return 任何值都不报错,但 forEach 会忽略它
// void vs undefined 关键区别
function returnsUndefined(): undefined {
return undefined; // 必须 return,否则报错
}
function returnsVoid(): void {
// 不需要 return,隐式返回 undefined
}
// 实际应用:React 事件处理
const onClick: () => void = () => {
return false; // TS 不报错,void 允许返回任意值
};
// 如果类型是 () => undefined,就必须 return undefined
void 返回时,传入的实现函数可以返回任何值,但调用方拿到的返回值类型是 void(不能使用)。这是 TS 有意为之的"宽松实现、严格使用"设计。
拓宽与收窄进阶
理解 TS 的类型拓宽机制与应对策略
// 拓宽陷阱 1:数组字面量
const roles = ["admin", "user"];
// 推断为 string[],不是 ("admin" | "user")[]
// 修复:as const
const roles2 = ["admin", "user"] as const;
// 类型为 readonly ["admin", "user"]
// 拓宽陷阱 2:对象属性
const config = { env: "production" };
// config.env 的类型是 string,不是 "production"
function deploy(env: "production" | "staging") {}
// deploy(config.env); // 报错!string 不能赋给字面量联合
// 修复方案汇总
// 方案 1:as const 锁定整个对象
const config1 = { env: "production" } as const;
// 方案 2:as const 只锁定某个属性
const config2 = { env: "production" as const };
// 方案 3:显式类型注解
const config3: { env: "production" | "staging" } = { env: "production" };
// 对象字面量新鲜度(Freshness)
function setUser(user: { name: string }) {}
setUser({ name: "张三" }); // OK
// setUser({ name: "张三", age: 25 }); // 报错!多余属性
const obj = { name: "张三", age: 25 };
setUser(obj); // OK!变量赋值不做多余属性检查
{ nmae: "张三" } 会被立即捕获。
拓宽陷阱实战
实际项目中容易踩的类型拓宽坑
// 陷阱 1:条件判断中的拓宽
const result = Math.random() > 0.5 ? "yes" : "no";
// result 类型是 string,不是 "yes" | "no"
// 修复:
const result2 = Math.random() > 0.5 ? "yes" as const : "no" as const;
// 或者:const result = ... as const
// 陷阱 2:函数返回值拓宽
function getStatus() {
return "loading"; // 推断为 string
}
type S = ReturnType; // string,不是 "loading"
// 修复:显式标注返回类型
function getStatus2(): "loading" | "done" { return "loading"; }
// 陷阱 3:解构时丢失 as const
const point = { x: 1, y: 2 } as const;
const { x, y } = point;
// x 的类型是 1,y 的类型是 2 —— 解构保留字面量 ✓
// 但 let 解构会拓宽!
let { x: lx, y: ly } = point;
// lx 类型是 1,ly 类型是 2(const 推断不变)
// 但如果是 let 声明的变量,赋值后可以改变
// 陷阱 4:null/undefined 也参与拓宽
let val = null; // 推断为 any(严格模式下)
let val2: string | null = null; // 正确做法
as const 最可靠;函数返回值尽量显式标注;let 声明的变量会被拓宽为宽泛类型。
const 类型参数
TS 5.0+ 用 <const T> 自动保留字面量
以前要保留字面量类型必须写 as const,TS 5.0 引入了 <const T> 让泛型函数自动保留字面量。
// 没有 const 类型参数
function getRoles(roles: T[]): T[] {
return roles;
}
const r1 = getRoles(["admin", "user"]);
// r1 的类型是 string[],字面量信息丢失了
// 使用 const 类型参数
function getRoles(roles: T[]): T[] {
return roles;
}
const r2 = getRoles(["admin", "user"]);
// r2 的类型是 ("admin" | "user")[],字面量保留!
// 实际应用:创建路由表
function createRoutes>(routes: T): T {
return routes;
}
const routes = createRoutes({
home: "/",
about: "/about",
contact: "/contact",
});
// routes.home 的类型是 "/" 而非 string
<const T> 的效果等同于调用方写了 as const,但好处是把决定权放在了函数定义端,调用方不用操心。适合工具函数、配置构建器等场景。
const T 对比
有无 <const T> 的行为差异
| 写法 | 推断结果 | 字面量是否保留 |
|---|---|---|
<T> | string / number | 否,拓宽为宽泛类型 |
<const T> | "admin" / 42 | 是,保留字面量 |
<T>as const | "admin" / 42 | 是,但调用方需手动加 |
// 场景:类型安全的事件监听器
function on(
event: T,
handler: (payload: EventMap[T]) => void,
) {
// event 类型保留为字面量
// handler 的参数类型根据 event 精确定位
}
on("click", (e) => { /* e: ClickEvent */ });
on("keyup", (e) => { /* e: KeyboardEvent */ });
// on("tap", ...); // 报错!EventMap 中没有 "tap"
<const T> 让函数定义者决定是否保留字面量,而不是依赖调用方记住写 as const。配合 extends 约束,可以实现非常精确的类型推导。
综合练习
定义一个含有多种类型的用户配置对象
任务:根据下面的需求,定义完整的类型并创建对象。
// 你的代码:定义 UserConfig 类型,然后创建一个对象
参考答案
type Theme = "light" | "dark";
type Coords = [number, number]; // [经度, 纬度]
interface UserConfig {
username: string;
age: number;
isActive: boolean;
tags: string[];
theme: Theme;
email: string | null; // 可选的,可能为空
coords: Coords; // 元组:经纬度
}
const config: UserConfig = {
username: "张三",
age: 25,
isActive: true,
tags: ["开发者", "前端"],
theme: "dark",
email: null,
coords: [116.4, 39.9],
};
// 试试这些操作,看看 TS 的反应:
// config.theme = "blue"; // 报错!只能是 light 或 dark
// config.coords = [116.4]; // 报错!元组长度不对
// config.email = 123; // 报错!不是 string 或 null
常见类型错误
问题一:隐式 any 报错
原因:函数参数没有标注类型,TS 不知道它是什么。
修复:给参数加上类型注解,例如 name: string。
问题二:不能将 null 赋值给 string
原因:strictNullChecks 开启后,null 不能赋给其他类型。
修复:使用联合类型 string | null。
问题三:类型 "string | number" 上不存在属性 "toFixed"
原因:联合类型只能使用所有成员共有的属性。
修复:先用 typeof 收窄类型,再调用专属方法。
试一试
练习一
任务:给下面的变量添加类型注解。
// 给每个变量加上合适的类型注解
let title = "TypeScript入门";
let pages = 320;
let published = true;
let tags = ["编程", "前端"];
参考答案
let title: string = "TypeScript入门";
let pages: number = 320;
let published: boolean = true;
let tags: string[] = ["编程", "前端"];
练习二
任务:定义一个函数,接收价格和折扣率,返回打折后的价格。加上类型注解。
// 你的代码:
参考答案
function discount(price: number, rate: number): number {
return price * (1 - rate);
}
discount(100, 0.2); // 80
综合挑战
综合运用 never、断言、const 类型参数
as const 或 <const T> 保留字面量类型。
// 你的代码:完成类型定义和函数实现
参考答案
// 步骤 1:用 as const 定义事件映射表
const EVENTS = {
click: { target: "button", x: 0, y: 0 },
keydown: { key: "Enter", code: "Enter" },
scroll: { direction: "down", distance: 100 },
} as const;
// 步骤 2:提取事件名和载荷类型
type EventName = keyof typeof EVENTS;
type EventPayload = typeof EVENTS[E];
// 步骤 3:分发函数——穷尽检查
function assertNever(x: never): never {
throw new Error("未处理的事件: " + x);
}
function dispatch(
event: E,
payload: EventPayload,
): void {
switch (event) {
case "click":
console.log(`点击位置: (${payload.x}, ${payload.y})`);
break;
case "keydown":
console.log(`按键: ${payload.key}`);
break;
case "scroll":
console.log(`滚动: ${payload.direction} ${payload.distance}px`);
break;
default:
assertNever(event); // 新增事件时会报错
}
}
// 步骤 4:类型安全调用
dispatch("click", { target: "div", x: 100, y: 200 }); // OK
dispatch("keydown", { key: "Escape", code: "Escape" }); // OK
// dispatch("click", { key: "a" }); // 报错!载荷不匹配
// dispatch("hover", {}); // 报错!不存在的事件
这个练习串联了本章的核心知识点:as const 保留字面量、keyof 提取联合类型、泛型约束、never 穷尽检查、void 返回类型。能写出来,说明本章内容你已经吃透了。
📝 要点总结
本章核心知识点
原始类型
number、string、boolean、bigint、symbol、null、undefined
特殊类型
unknown 安全顶层、any 关闭检查、never 穷尽、void 无返回
类型推断
TS 自动推断,函数返回值、上下文类型可省略标注
类型断言
as 语法、非空断言 !、const 断言 as const
元组与字面量
固定长度元组、字面量类型限定取值范围
下一章
掌握了基础类型,下一章学习函数类型全解——参数、返回值、重载、类型谓词。