📚 全栈开发学习系列 · 阶段三:前端深化
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 应用
关注公众号持续获取系列更新