目标:从零开始掌握 TypeScript 的类型系统,并学会在 React 中编写类型安全的 JSX 代码。
目录
- [TypeScript 简介](#TypeScript 简介)
- 基础类型系统
- 接口与类型别名
- 函数与泛型
- [JSX 是什么](#JSX 是什么)
- [TSX:TypeScript 中的 JSX](#TSX:TypeScript 中的 JSX)
- [React + TypeScript 实战](#React + TypeScript 实战)
- 常见类型定义技巧
- 最佳实践与避坑指南
1. TypeScript 简介
1.1 什么是 TypeScript
TypeScript(简称 TS)是 JavaScript 的超集,由微软开发。它在 JS 的基础上添加了静态类型系统。
typescript
// JavaScript(运行时才能发现错误)
function add(a, b) {
return a + b;
}
add(1, "2"); // 不报错,但结果是 "12"(字符串拼接)
// TypeScript(编译时就能发现错误)
function add(a: number, b: number): number {
return a + b;
}
add(1, "2"); // ❌ 编译错误:类型 'string' 不能赋值给类型 'number'
1.2 为什么需要 TypeScript
- 更早发现错误:编译阶段捕获类型错误,而非运行时
- 更好的 IDE 支持:智能提示、自动补全、重构支持
- 可读性与可维护性:类型即文档,方便团队协作
- 更安全的重构:修改代码时,类型系统会提示所有受影响的位置
1.3 安装与运行
bash
# 全局安装 TypeScript
npm install -g typescript
# 编译单个文件
tsc hello.ts
# 初始化 tsconfig.json(项目配置)
tsc --init
# 使用 ts-node 直接运行(开发时方便)
npm install -D ts-node
ts-node hello.ts
1.4 tsconfig.json 基础配置
json
{
"compilerOptions": {
"target": "ES2020", // 编译目标 JS 版本
"module": "ESNext", // 模块系统
"jsx": "react-jsx", // JSX 转换模式
"strict": true, // 启用所有严格类型检查
"esModuleInterop": true, // 兼容 CommonJS/ES Module
"skipLibCheck": true, // 跳过 .d.ts 类型检查
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
2. 基础类型系统
2.1 原始类型
typescript
// 布尔值
let isDone: boolean = false;
// 数字(支持十进制、十六进制、二进制、八进制)
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
// 字符串
let color: string = "blue";
let fullName: string = `Bob Bobbington`;
let sentence: string = `Hello, my name is ${fullName}.`;
// 空值
function warnUser(): void {
console.log("This is a warning message");
}
// null 和 undefined
let u: undefined = undefined;
let n: null = null;
2.2 any 与 unknown
typescript
// any:绕过类型检查,尽量少用
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
// unknown:类型安全的 any,使用前必须进行类型检查
let userInput: unknown = getSomeInput();
if (typeof userInput === "string") {
console.log(userInput.toUpperCase()); // ✅ 安全
}
// userInput.toUpperCase(); // ❌ 直接调用会报错
2.3 数组与元组
typescript
// 数组:两种声明方式
let list1: number[] = [1, 2, 3];
let list2: Array<string> = ["a", "b", "c"];
// 元组:固定长度、固定类型的数组
let person: [string, number] = ["Alice", 25];
// person = [25, "Alice"]; // ❌ 顺序错误
// 具名元组(更清晰)
type Point = [x: number, y: number];
let p: Point = [10, 20];
2.4 对象类型
typescript
// 直接声明对象形状
let user: { name: string; age: number } = {
name: "Tom",
age: 20,
};
// 可选属性
let config: { host: string; port?: number } = {
host: "localhost",
};
// 只读属性
let readonlyUser: { readonly id: number; name: string } = {
id: 1,
name: "Tom",
};
// readonlyUser.id = 2; // ❌ 无法修改
2.5 联合类型与交叉类型
typescript
// 联合类型:值可以是多种类型之一
let value: string | number = "hello";
value = 42;
// value = true; // ❌ 错误
// 类型收窄(Type Narrowing)
function printId(id: string | number) {
if (typeof id === "string") {
console.log(id.toUpperCase()); // TS 知道这里是 string
} else {
console.log(id.toFixed(2)); // TS 知道这里是 number
}
}
// 交叉类型:合并多个类型
type Employee = { name: string; id: number };
type Manager = { department: string };
type ManagerEmployee = Employee & Manager;
let manager: ManagerEmployee = {
name: "Alice",
id: 1,
department: "Engineering",
};
3. 接口与类型别名
3.1 Interface(接口)
typescript
interface User {
id: number;
name: string;
email?: string; // 可选属性
readonly createdAt: Date;
}
function createUser(user: User): User {
return user;
}
const newUser = createUser({
id: 1,
name: "John",
createdAt: new Date(),
});
3.2 接口的扩展
typescript
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
const myDog: Dog = {
name: "Buddy",
breed: "Golden Retriever",
};
3.3 Type Alias(类型别名)
typescript
type UserID = string | number;
type Point = { x: number; y: number };
// 类型别名也可以表示联合类型
type Status = "pending" | "success" | "error";
let currentStatus: Status = "success";
// currentStatus = "unknown"; // ❌ 不在允许的范围内
3.4 Interface vs Type
| 特性 | interface |
type |
|---|---|---|
| 扩展方式 | extends |
&(交叉类型) |
| 同名合并 | ✅ 自动声明合并 | ❌ 不可重复定义 |
| 联合类型 | ❌ 不支持 | ✅ 支持 |
| 映射类型 | ❌ 不支持 | ✅ 支持 |
typescript
// interface 自动声明合并
interface Window {
myApp: string;
}
interface Window {
version: number;
}
// Window 现在有 myApp 和 version 两个属性
建议 :定义对象形状时优先用 interface,需要联合类型或复杂类型运算时用 type。
4. 函数与泛型
4.1 函数类型
typescript
// 命名函数
function add(x: number, y: number): number {
return x + y;
}
// 箭头函数
const multiply = (x: number, y: number): number => x * y;
// 可选参数和默认参数
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
// 剩余参数
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// 函数类型表达式
let binaryOp: (a: number, b: number) => number;
binaryOp = add;
binaryOp = multiply;
4.2 泛型(Generics)
泛型让你编写类型参数化的代码,实现代码复用和类型安全。
typescript
// 无泛型:只能处理一种类型,或丢失类型信息
function identity(arg: any): any {
return arg;
}
// 有泛型:保留传入的类型
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("myString");
let output2 = identity(123); // TS 自动推断类型为 number
4.3 泛型约束
typescript
// 约束 T 必须具有 length 属性
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength("hello"); // ✅ string 有 length
logLength([1, 2, 3]); // ✅ 数组有 length
// logLength(123); // ❌ number 没有 length
4.4 泛型接口与类
typescript
// 泛型接口
interface GenericResponse<T> {
data: T;
status: number;
message: string;
}
const userResponse: GenericResponse<User> = {
data: { id: 1, name: "Tom", createdAt: new Date() },
status: 200,
message: "OK",
};
// 泛型类
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
const numberContainer = new Container<number>(42);
const stringContainer = new Container("hello"); // 类型推断
5. JSX 是什么
5.1 JSX 语法
JSX(JavaScript XML)是一种 JavaScript 的语法扩展,允许在 JS 中编写类似 HTML 的结构。
jsx
// JSX
const element = <h1 className="greeting">Hello, world!</h1>;
// 编译后(大致等效于)
const element = React.createElement(
"h1",
{ className: "greeting" },
"Hello, world!"
);
5.2 JSX 规则
-
必须有一个根元素
jsx// ❌ 错误:Adjacent JSX elements must be wrapped return ( <h1>Title</h1> <p>Content</p> ); // ✅ 正确 return ( <div> <h1>Title</h1> <p>Content</p> </div> ); // ✅ 或者用 Fragment return ( <> <h1>Title</h1> <p>Content</p> </> ); -
使用 camelCase 属性名
jsx// HTML 中的 class 变成 className <div className="container" tabIndex={0} /> -
大括号内嵌 JS 表达式
jsxconst name = "Alice"; const element = <h1>Hello, {name}</h1>; // 可以写任意表达式 const element2 = <h1>1 + 1 = {1 + 1}</h1>; -
标签必须闭合
jsx// ✅ <img src="photo.jpg" alt="Photo" /> <input type="text" />
6. TSX:TypeScript 中的 JSX
6.1 文件扩展名
.ts--- 普通 TypeScript 文件.tsx--- 包含 JSX 的 TypeScript 文件
6.2 tsconfig 中的 JSX 配置
json
{
"compilerOptions": {
"jsx": "react-jsx", // React 17+ 新 JSX 转换
// "jsx": "react", // 传统转换:需要 import React
"jsxImportSource": "react" // 自定义 JSX 运行时
}
}
6.3 为 JSX 元素指定类型
在 TSX 中,HTML 标签有内置的类型定义。
tsx
// 原生 DOM 元素的类型
const button: JSX.Element = <button>Click me</button>;
// HTML 属性类型
const inputProps: React.InputHTMLAttributes<HTMLInputElement> = {
type: "text",
placeholder: "Enter name",
onChange: (e) => console.log(e.target.value),
};
6.4 React 类型定义安装
bash
npm install -D @types/react @types/react-dom
7. React + TypeScript 实战
7.1 函数组件
tsx
import React from "react";
// 定义 Props 类型
interface GreetingProps {
name: string;
age?: number; // 可选属性
}
// 方式一:显式声明 props 类型
function Greeting({ name, age }: GreetingProps) {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>You are {age} years old.</p>}
</div>
);
}
// 方式二:使用 React.FC(Functional Component)
const Greeting2: React.FC<GreetingProps> = ({ name, age }) => {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>You are {age} years old.</p>}
</div>
);
};
// 使用
function App() {
return (
<>
<Greeting name="Alice" age={25} />
<Greeting name="Bob" /> {/* age 是可选的 */}
{/* <Greeting /> */} {/* ❌ 缺少必需的 name 属性 */}
</>
);
}
注意 :现代 React 开发中,
React.FC的使用存在争议,因为它隐式包含children属性。许多团队更倾向于直接为 props 参数标注类型。
7.2 带 children 的组件
tsx
interface CardProps {
title: string;
children: React.ReactNode; // 接受任何可渲染内容
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// 使用
function App() {
return (
<Card title="Welcome">
<p>This is some content inside the card.</p>
<button>Action</button>
</Card>
);
}
children 类型选择:
React.ReactNode--- 最宽泛,接受任何可渲染内容(推荐)React.ReactElement--- 只接受 JSX 元素string/number--- 仅接受特定类型
7.3 useState Hook
tsx
import { useState } from "react";
function Counter() {
// TS 自动推断类型为 number
const [count, setCount] = useState(0);
// 显式指定类型(初始值为 null/undefined 时必需)
const [user, setUser] = useState<User | null>(null);
// 复杂对象
interface FormState {
username: string;
password: string;
isLoading: boolean;
}
const [form, setForm] = useState<FormState>({
username: "",
password: "",
isLoading: false,
});
const handleUpdate = (field: keyof FormState, value: string | boolean) => {
setForm((prev) => ({ ...prev, [field]: value }));
};
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
<input
value={form.username}
onChange={(e) => handleUpdate("username", e.target.value)}
/>
</div>
);
}
7.4 useRef Hook
tsx
import { useRef, useEffect } from "react";
function TextInputWithFocusButton() {
// DOM 引用
const inputRef = useRef<HTMLInputElement>(null);
// 存储不触发重新渲染的值
const renderCount = useRef<number>(0);
useEffect(() => {
renderCount.current++;
});
const handleClick = () => {
// inputRef.current 可能是 null,需要可选链或类型守卫
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} type="text" />
<button onClick={handleClick}>Focus the input</button>
<p>Rendered {renderCount.current} times</p>
</>
);
}
7.5 事件处理
tsx
import { useState } from "react";
function Form() {
const [value, setValue] = useState("");
// 输入事件
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
};
// 表单提交事件
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log("Submitted:", value);
};
// 鼠标事件
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log("Clicked at:", event.clientX, event.clientY);
};
// 键盘事件
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
console.log("Enter pressed");
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
<button type="submit" onClick={handleClick}>
Submit
</button>
</form>
);
}
常见事件类型速查表:
| 事件 | 类型 |
|---|---|
| onChange (input) | React.ChangeEvent<HTMLInputElement> |
| onClick | React.MouseEvent<HTMLButtonElement> |
| onSubmit | React.FormEvent<HTMLFormElement> |
| onKeyDown | React.KeyboardEvent<HTMLInputElement> |
| onScroll | React.UIEvent<HTMLDivElement> |
| onFocus / onBlur | React.FocusEvent<HTMLInputElement> |
7.6 自定义 Hook
tsx
import { useState, useEffect } from "react";
// 泛型 Hook:获取异步数据
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// 使用
interface User {
id: number;
name: string;
email: string;
}
function UserList() {
const { data: users, loading, error } = useFetch<User[]>("/api/users");
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
if (!users) return null;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
8. 常见类型定义技巧
8.1 为第三方库补充类型
当某个库没有类型定义时:
typescript
// types/my-untyped-lib.d.ts
declare module "some-untyped-library" {
export function doSomething(input: string): number;
export const version: string;
}
8.2 全局类型声明
typescript
// types/global.d.ts
declare global {
interface Window {
myApp: {
version: string;
config: Record<string, unknown>;
};
}
}
// 现在可以在代码中使用
window.myApp.version;
8.3 实用工具类型(Utility Types)
typescript
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Partial:所有属性变为可选
type PartialUser = Partial<User>;
// Pick:选取指定属性
type UserPreview = Pick<User, "id" | "name">;
// Omit:排除指定属性
type PublicUser = Omit<User, "password">;
// Required:所有属性变为必需
type StrictUser = Required<PartialUser>;
// Record:构建键值对类型
type UsersById = Record<number, User>;
// ReturnType:提取函数返回类型
type ApiResponse = ReturnType<typeof fetchUser>;
// Parameters:提取函数参数类型
type FetchUserParams = Parameters<typeof fetchUser>;
8.4 组件 Props 提取
tsx
import { ComponentProps } from "react";
// 提取原生 button 的所有 props
type ButtonProps = ComponentProps<"button">;
// 在自定义组件中扩展原生属性
interface MyButtonProps extends ComponentProps<"button"> {
variant?: "primary" | "secondary" | "danger";
isLoading?: boolean;
}
function MyButton({ variant = "primary", isLoading, children, ...rest }: MyButtonProps) {
return (
<button className={`btn btn-${variant}`} disabled={isLoading} {...rest}>
{isLoading ? "Loading..." : children}
</button>
);
}
9. 最佳实践与避坑指南
9.1 ✅ 推荐做法
-
启用严格模式
json{ "compilerOptions": { "strict": true } } -
优先使用
interface定义对象,用type定义联合/工具类型 -
避免使用
anytypescript// ❌ 不好 function process(data: any) { ... } // ✅ 好 function process<T>(data: T) { ... } // 或 function process(data: unknown) { ... } -
使用
satisfies关键字(TS 4.9+)typescriptconst config = { host: "localhost", port: 3000, } satisfies { host: string; port: number }; -
为 Hook 依赖项提供完整类型
typescriptuseEffect(() => { // ... }, [userId]); // TS 会检查 userId 类型是否稳定
9.2 ❌ 常见错误
| 错误 | 说明 | 修正 |
|---|---|---|
any 滥用 |
失去类型保护 | 用 unknown + 类型守卫替代 |
可选链忘记处理 null |
user?.name 整体可能是 undefined |
提供默认值或类型守卫 |
忘记给 useState 传泛型 |
useState(null) 推断为 null 类型 |
`useState<string |
as 类型断言滥用 |
强制转换可能不安全 | 优先用类型守卫缩小类型 |
| 事件类型写错 | e: Event 太宽泛 |
用 React.MouseEvent<HTMLButtonElement> |
9.3 快速排错指南
typescript
// 问题:Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'XXX'
// 解决:使用 Record 或添加索引签名
interface Config {
[key: string]: string;
}
// 问题:Type 'string | undefined' is not assignable to type 'string'
// 解决:非空断言(确定有值时)或提供默认值
const name = user.name ?? "Anonymous";
// 或
const name = user.name!; // 确定不会为 null/undefined
// 问题:No overload matches this call
// 解决:检查函数重载定义,确保参数类型匹配
附录:学习资源
- TypeScript 官方文档
- React + TypeScript 速查表
- Type Challenges --- 类型体操练习题
- Total TypeScript --- Matt Pocock 的 TS 教程
恭喜!完成本教程后,你已经掌握了 TypeScript 的核心类型系统和在 React 中使用 JSX 的基础知识。建议通过实际项目练习来巩固这些概念。