📚 全栈开发学习系列 · 阶段三:前端深化
17 React入门:组件化思想与JSX语法 ✅
18 React进阶:状态管理与副作用钩子 ✅
19 TypeScript入门:类型系统与类型注解(当前篇)
20 Next.js基础:SSR与路由体系
21 Tailwind CSS:原子化样式开发
22 前端工程化:Vite构建与打包
23 前端测试:单元测试与API测试
TypeScript 入门:类型系统与类型注解
难度:进阶 | 主题色:#2563eb | TypeScript 7.0
读完本篇你将能:理解 TypeScript 与 JavaScript 的关系,掌握基础类型注解写法,用接口和类型别名描述对象结构,使用联合类型和泛型编写灵活且安全的代码,并在 React 组件中正确标注 Props 和 State 的类型。
你在前两篇文章中写了大量 React 组件,用 useState 管理计数器、用 useEffect 处理副作用。但你可能遇到了一种困惑:传给组件的 props 到底是什么结构?某个函数的参数应该是字符串还是数字?代码跑起来才报错,调试时只能在浏览器控制台里猜类型。
这就是 JavaScript 的痛点——动态类型意味着类型错误要等到运行时才暴露。TypeScript 的出现就是为了解决这个问题:它在 JavaScript 之上加了一层静态类型系统,让你在编写代码时就能发现类型不匹配的错误。就像文字处理软件的拼写检查——你在敲字时就标红拼写错误,而不是把文档打印出来后才发现错别字。
📑 本文目录
01TypeScript 是什么:给 JavaScript 穿上类型铠甲
02安装与第一个 TypeScript 程序
03基础类型注解:变量、函数参数与返回值
04接口与类型别名:描述对象形状
05联合类型与交叉类型:灵活组合
06泛型基础:类型层面的函数
07在 React 中使用 TypeScript
01 TypeScript 是什么:给 JavaScript 穿上类型铠甲
JavaScript 是动态类型语言:变量的类型在运行时确定,同一变量可以先存字符串再存数字,语言层面不做任何限制。这种灵活性在小型项目中很方便,但项目规模一大,就成了隐患的温床——你调用一个函数,传错了参数类型,JavaScript 不会报错,直到代码执行到那一步才抛出 TypeError。
TypeScript 是 Microsoft 开发的 JavaScript 超集(superset)。"超集"意味着:所有合法的 JavaScript 代码都是合法的 TypeScript 代码,TypeScript 在此基础上增加了类型注解语法。TypeScript 编译器(tsc)会检查类型是否匹配,然后把类型注解剥离,输出纯净的 JavaScript 代码——浏览器和 Node.js 运行的还是 JavaScript。
| 维度 |
JavaScript |
TypeScript |
| 类型检查 |
运行时 |
编写时(编译期) |
| 类型错误暴露 |
代码跑起来才报错 |
编辑器即时标红 |
| IDE 补全 |
基于推断,不精确 |
基于类型,精确可靠 |
| 学习曲线 |
平缓 |
较陡,但收益高 |
2026年7月,TypeScript 7.0 正式发布。这个版本的核心变化是将编译器从 TypeScript 本身重写为原生 Go 语言实现,编译速度提升了 8-12 倍。一个原本需要 77 秒完成全量编译的大型项目,在 TS 7.0 下只需 7.5 秒。TS 6.0(2026年3月)已将 strict 模式设为默认开启,TS 7.0 延续了这一策略——新项目必须全量开启类型检查,不再有关闭选项。
💡 小贴士
TypeScript 7.0 可以与 6.0 并行安装。现有项目可以先用 6.0 迁移到 strict 模式,再平滑升级到 7.0 获取性能提升。安装命令:npm install -g typescript@7
02 安装与第一个 TypeScript 程序
理解了 TypeScript 的定位后,让我们动手安装编译器,编写并编译第一个 .ts 文件。TypeScript 的工具链非常简洁——一个 tsc 命令搞定编译。
安装 TypeScript 编译器:
Shell
npm install -g typescript@7
tsc --version
输出 Version 7.0.3 表示安装成功。现在创建第一个 TypeScript 文件 hello.ts:
TypeScript
const greeting: string = "Hello, TypeScript!";
function repeat(text: string, times: number): string {
return text.repeat(times);
}
console.log(repeat(greeting, 2));
注意第1行和第3行中的冒号语法:greeting: string 声明了 greeting 变量的类型为字符串,text: string, times: number 声明了函数参数的类型,: string 在参数列表后声明了返回值类型。
编译并运行:
Shell
tsc hello.ts
node hello.js
输出 Hello, TypeScript!Hello, TypeScript!。tsc 把 hello.ts 编译成 hello.js,剥离了所有类型注解,Node.js 运行的是纯 JavaScript。
实际项目中不会逐文件编译,而是用 tsconfig.json 配置编译选项。TS 7.0 的推荐配置:
JSON
|
1
2
3
4
5
6
7
8
9
10
11
|
{
"compilerOptions": {
"target": "es2025",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
|
各选项含义:target 指定编译输出的 JS 版本(es2025 支持最新语法);module 指定模块系统;strict: true 开启全部严格类型检查(TS 6.0+ 默认开启,无法关闭);skipLibCheck 跳过第三方库的类型检查以加速编译。
⚠️ 常见错误
"target": "es5" — TS 7.0 移除了 ES5 编译目标,最低支持 ES2015
✓ 正确:将 target 设为 "es2015" 或更高版本
03 基础类型注解:变量、函数参数与返回值
你已经在上面的示例中见过类型注解的基本写法:变量名: 类型。现在系统地学习 TypeScript 的基础类型体系。
原始类型
TypeScript 的原始类型与 JavaScript 一一对应:
TypeScript
|
1
2
3
4
5
6
7
8
9
|
let username: string = "alice";
let age: number = 28;
let isActive: boolean = true;
// 数组两种写法
let scores: number[] = [90, 85, 92];
let names: Array<string> = ["bob", "carol"];
// 元组:固定长度和类型的数组
|
数组有两种等价写法:number[] 和 Array<number>,后者是泛型写法。元组(Tuple)是特殊数组,每个位置的类型固定:
TypeScript
let record: [string, number] = ["alice", 28];
console.log(record[0]); // "alice"
console.log(record[1]); // 28
特殊类型:any、unknown、void、never
| 类型 |
含义 |
使用场景 |
any |
跳过类型检查 |
迁移旧代码时临时使用 |
unknown |
安全的 any,使用前必须收窄 |
接收外部不确定类型的数据 |
void |
函数无返回值 |
回调函数、副作用函数 |
never |
不可能到达的值 |
抛异常的函数、无限循环 |
其中 unknown 是 any 的安全替代品。在 strict 模式下,你不能直接对 unknown 类型的值做任何操作,必须先用类型守卫(typeof、instanceof)收窄类型。这强制你处理类型不确定性,而不是用 any 糊弄过去。
函数类型注解
函数是 TypeScript 类型注解最常出现的场景——参数和返回值都需要标注:
TypeScript
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function add(a: number, b: number): number {
return a + b;
}
// 箭头函数写法
const multiply = (a: number, b: number): number => a * b;
// 可选参数:参数名后加 ?
function greet(name: string, greeting?: string): string {
return `${greeting ?? "Hello"}, ${name}`;
}
console.log(greet("alice")); // "Hello, alice"
|
可选参数 greeting?: string 表示该参数可以不传,类型实际上是 string | undefined。?? 是空值合并运算符——左侧为 null 或 undefined 时才取右侧值。
类型推断
TypeScript 不是所有变量都必须手动标注类型。当你在声明变量时直接赋值,编译器会自动推断类型:
TypeScript
let count = 10; // 推断为 number
let message = "hi"; // 推断为 string
// count = "hello"; // 编译错误:不能将 string 赋给 number
推断机制意味着你不必到处写类型注解。原则是:能推断的不写,函数参数和返回值这种无法从上下文推断的必须写。这既保证了类型安全,又避免了类型注解的噪音。
编写 .ts 代码
→
tsc 类型检查
→
输出 .js 运行
04 接口与类型别名:描述对象形状
掌握了原始类型后,你面临一个现实问题:JavaScript 中大量使用对象,而对象的形状(有哪些字段、字段是什么类型)正是类型错误的温床。TypeScript 提供了两种工具来描述对象形状:interface 和 type。
接口(interface)
接口用来定义对象的形状约定。下面定义一个用户接口:
TypeScript
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
interface User {
id: number;
name: string;
email: string;
age?: number; // 可选属性
readonly role: string; // 只读属性
}
const user: User = {
id: 1,
name: "alice",
email: "alice@example.com",
role: "admin"
};
// user.role = "user"; // 编译错误:只读属性不能修改
// const admin: User = { id: 2, name: "bob" }; // 缺少 email、role
|
两个关键语法:age?: number 中的问号表示该属性可选,可以不写;readonly 表示该属性只能在初始化时赋值,之后不可修改。
类型别名(type)
type 关键字可以给任何类型起别名,包括原始类型、联合类型、对象类型等:
TypeScript
type ID = number | string;
type Point = { x: number; y: number };
let userId: ID = 1001; // number 合法
userId = "U1001"; // string 也合法
interface vs type:什么时候用哪个
| 特性 |
interface |
type |
| 对象形状 |
✓ 首选 |
✓ 可以 |
| 联合/交叉类型 |
✗ 不支持 |
✓ 首选 |
| 扩展(继承) |
extends 关键字 |
& 交叉类型 |
| 同名合并 |
✓ 自动合并 |
✗ 报错 |
实用原则:描述对象形状用 interface,需要联合类型、交叉类型或给原始类型起别名时用 type。两者大多数场景可互换,选你团队习惯的即可。
接口扩展
接口可以用 extends 继承其他接口,形成层级结构:
TypeScript
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
interface Animal {
name: string;
move(): void;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
const rex: Dog = {
name: "Rex",
breed: "Labrador",
move() { console.log("walking..."); },
bark() { console.log("Woof!"); }
|
Dog 继承了 Animal 的 name 和 move,同时添加了自己的 breed 和 bark。这种层级关系让类型定义更清晰、可复用。
05 联合类型与交叉类型:灵活组合
接口和类型别名描述了单个对象的形状,但真实世界的数据更复杂:一个 ID 可能是数字也可能是字符串,一个管理员同时是用户和权限持有者。TypeScript 用联合类型和交叉类型处理这类场景。
联合类型
联合类型用 | 表示"或"关系——值可以是多种类型之一:
TypeScript
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 字面量联合类型
type Status = "success" | "error" | "loading";
function handleStatus(status: Status) {
if (status === "success") {
console.log("操作成功");
} else if (status === "error") {
console.log("操作失败");
} else {
console.log("加载中...");
}
}
handleStatus("success"); // 合法
// handleStatus("pending"); // 编译错误:不在联合范围内
// 类型守卫:typeof 收窄联合类型
|
字面量联合类型 "success" | "error" | "loading" 是 TypeScript 的利器——它把合法值限定在几个字符串中,传错值会直接编译报错。这在 API 响应状态、配置选项等场景中极其有用。
💡 小贴士
联合类型需要配合类型守卫使用。TypeScript 在 if (typeof x === "string") 分支内会自动把 x 的类型收窄为 string,这种机制叫做"类型收窄"(narrowing),是处理联合类型的核心手段。
交叉类型
交叉类型用 & 表示"且"关系——值必须同时满足多个类型:
TypeScript
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
interface WithId { id: number; }
interface WithTimestamp { createdAt: Date; }
// 同时拥有 id 和 createdAt
type Record = WithId & WithTimestamp;
const log: Record = {
id: 1,
createdAt: new Date()
};
console.log(log.id, log.createdAt);
// 1 2026-08-18T10:30:00.000Z
|
06 泛型基础:类型层面的函数
到目前为止你学到的类型都是固定的——add(a: number, b: number) 只能处理数字。但很多函数逻辑上能处理多种类型:identity 函数返回传入的值,不管传的是字符串还是数字都应该能工作。用 any 会丢失类型信息——这就是泛型要解决的问题。
泛型(Generics)是类型的参数化。你可以把泛型理解为"类型层面的函数"——普通函数的参数是值,泛型的参数是类型。
TypeScript
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// 泛型函数:T 是类型参数
function identity<T>(value: T): T {
return value;
}
// 调用时指定类型参数
const str = identity<string>("hello"); // string
const num = identity<number>(42); // number
// 类型推断:不写 <string> 也能推断出来
const inferred = identity("world"); // 推断为 string
// 泛型约束:限制 T 必须有 length 属性
function getLength<T extends { length: number }>(item: T): number {
return item.length;
|
逐行解读关键语法:
identity<T> 中的 T 是类型参数——一个占位符,调用时用具体类型替换。identity<string>("hello") 把 T 替换为 string,函数签等价于 (value: string) => string。第14行的 extends { length: number } 是泛型约束——限制 T 必须有 length 属性,这样字符串、数组都能用,但数字不行。
泛型接口
接口也可以使用泛型。最常见的场景是数据容器的类型描述:
TypeScript
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// API 响应的通用结构
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// 用户列表响应
interface User { id: number; name: string; }
const userResp: ApiResponse<User[]> = {
code: 200,
message: "OK",
data: [{ id: 1, name: "alice" }]
};
|
ApiResponse<User[]> 表示 data 字段是 User 数组。如果换成 ApiResponse<string>,data 就是字符串。一个接口定义覆盖所有响应类型,这就是泛型的威力。
⚠️ 常见错误
function log(value: any): any { return value } — 用 any 会让类型信息丢失,调用后 IDE 无法补全
✓ 正确:function log<T>(value: T): T { return value },泛型保留类型信息
07 在 React 中使用 TypeScript
前两篇文章你用纯 JavaScript 写了 React 组件。现在把 TypeScript 加进来,让 React 组件获得类型保护。React 19 + TypeScript 已经是前端项目的标配——React 19 的官方类型定义已内置在 @types/react 中。
组件 Props 类型标注
用 interface 定义组件的 Props,让父组件传错 prop 时编译器直接报错:
TypeScript React
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
import { useState } from 'react';
interface TodoItemProps {
title: string;
completed: boolean;
onToggle?: () => void;
}
function TodoItem({ title, completed, onToggle }: TodoItemProps) {
return (
<div>
<input
type="checkbox"
checked={completed}
onChange={onToggle}
/>
<span>{title}</span>
</div>
);
}
// 使用:传错类型会编译报错
<TodoItem title={"Learn TS"} completed={false} />
|
useState 泛型标注
useState 是泛型函数,可以显式指定状态类型,也可以让 TypeScript 从初始值推断:
TypeScript React
// 自动推断为 number
const [count, setCount] = useState(0);
// 显式指定类型(初始值为 null 时必须显式标注)
const [user, setUser] = useState<User | null>(null);
第二个例子中,初始值是 null,不显式标注的话 TypeScript 会把 user 推断为 null 类型——之后无法赋值 User 对象。useState<User | null>(null) 正确告诉编译器状态可能是 User 也可能是 null。
事件类型
React 事件处理函数的参数需要标注事件类型。常用的事件类型:
TypeScript React
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { useState } from 'react';
function SearchBox() {
const [query, setQuery] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(`搜索:"${query}"`);
};
// JSX 省略...
}
|
两个关键事件类型:React.ChangeEvent<HTMLInputElement> 用于 input 的 onChange,React.FormEvent<HTMLFormElement> 用于 form 的 onSubmit。标注后 e.target.value 和 e.preventDefault() 都有精确的类型提示。
💡 小贴士
React 19 不再强制使用 React.FC 类型包裹组件。直接用普通函数 + Props 接口即可:function MyComp(props: MyProps) { ... },更简洁,类型提示也更直观。
📖 知识回顾
类型注解
interface
type别名
联合类型
交叉类型
泛型
类型推断
tsconfig
React Props
事件类型
✏️ 动手练习
🟢 基础验证
创建一个 formatPrice.ts 文件,定义一个函数 formatPrice(price: number, currency: string): string,返回带货币符号的价格字符串。编译并用 node 运行验证输出。预期输出:formatPrice(99.5, "$") → "$99.50"
🟡 组合应用
定义一个 Task 接口(id、title、completed、createdAt),再定义一个 TaskList 接口(tasks 数组、add 方法、getCompleted 方法)。编写函数实现这两个接口,确保传入错误类型时 tsc 报错。
🔴 开放挑战
编写一个泛型函数 groupBy<T, K extends string | number>(arr: T[], keyFn: (item: T) => K): Record<K, T[]>,按指定键将数组分组。提示:可以先实现具体类型版本(如按数字分组的 groupByNumber),再泛化为通用版本。
下篇预告
20 Next.js 基础:SSR 与路由体系
将学习服务端渲染(SSR)的工作原理,掌握 Next.js 的文件路由系统、数据获取方式,以及如何用 TypeScript 构建全栈 React 应用