第 110 章:TypeScript 在框架中的最佳实践
学习目标
- 掌握 TS 在 React、Vue、Node.js 中的最佳实践
- 学会组件 props、emit、ref 的类型标注
- 理解 API 路由、Prisma、ORM 的类型设计
- 能在企业级项目中规范使用 TS
一、React 中的 TS
1.1 函数组件 Props
typescript
import { ReactNode } from 'react';
// 基础 props
interface ButtonProps {
text: string;
onClick: () => void;
disabled?: boolean;
}
function Button({ text, onClick, disabled }: ButtonProps) {
return <button onClick={onClick} disabled={disabled}>{text}</button>;
}
// 带 children
interface CardProps {
title: string;
children: ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h3>{title}</h3>
{children}
</div>
);
}1.2 泛型组件
typescript
// 通用 List 组件
interface ListProps<T> {
items: T[];
renderItem: (item: T) => ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// 使用
<List<User>
items={users}
renderItem={(user) => <span>{user.name}</span>}
keyExtractor={(user) => user.id}
/>1.3 useState 类型
typescript
// 自动推断
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
// 显式标注(用于联合类型)
const [status, setStatus] = useState<'idle' | 'loading' | 'success'>('idle');
// 对象类型
interface User { id: number; name: string }
const [user, setUser] = useState<User | null>(null);1.4 useRef 类型
typescript
// DOM ref
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();
// 通用值 ref
const countRef = useRef<number>(0);
// 回调 ref
const measuredRef = useCallback((node: HTMLDivElement | null) => {
if (node) {
// 测量 DOM
}
}, []);1.5 Event 类型
typescript
// 表单事件
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
// 点击事件
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget);
};
// 键盘事件
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') { /* ... */ }
};1.6 Hooks 自定义
typescript
// useLocalStorage
function useLocalStorage<T>(key: string, initial: T): [T, (value: T) => void] {
const [value, setValue] = useState<T>(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initial;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// 使用
const [name, setName] = useLocalStorage('name', '');二、Vue 3 中的 TS
2.1 defineComponent
typescript
import { defineComponent, PropType } from 'vue';
interface User {
id: number;
name: string;
}
export default defineComponent({
name: 'UserCard',
props: {
user: {
type: Object as PropType<User>,
required: true
},
showEmail: {
type: Boolean,
default: false
}
},
emits: {
select: (id: number) => true, // 校验函数
delete: (id: number) => true
},
setup(props, { emit }) {
const handleClick = () => {
emit('select', props.user.id);
};
return { handleClick };
}
});2.2 <script setup> 语法
vue
<script setup lang="ts">
import { ref, computed } from 'vue';
interface User { id: number; name: string }
const props = defineProps<{
user: User;
size?: 'small' | 'medium' | 'large';
}>();
const emit = defineEmits<{
select: [id: number];
delete: [id: number];
}>();
const count = ref(0);
const doubled = computed(() => count.value * 2);
const handleClick = () => {
emit('select', props.user.id);
};
</script>2.3 ref / reactive 类型
typescript
import { ref, reactive } from 'vue';
// ref 自动推断
const count = ref(0); // Ref<number>
const name = ref(''); // Ref<string>
const user = ref<User | null>(null); // Ref<User | null>
// reactive
const state = reactive({
count: 0,
name: ''
});
// state.count 类型: number
// 数组
const list = ref<User[]>([]);
// list.value.push({ id: 1, name: 'Tom' });2.4 组合式函数
typescript
// useUser.ts
import { ref, Ref } from 'vue';
export function useUser(id: Ref<number> | number) {
const user = ref<User | null>(null);
const loading = ref(false);
const error = ref<Error | null>(null);
const fetch = async () => {
loading.value = true;
try {
const userId = typeof id === 'number' ? id : id.value;
const res = await fetch(`/api/users/${userId}`);
user.value = await res.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
};
return { user, loading, error, fetch };
}三、Node.js / Express 中的 TS
3.1 Express 请求处理
typescript
import express, { Request, Response, NextFunction } from 'express';
interface User {
id: number;
name: string;
}
// 扩展 Request
interface AuthRequest extends Request {
user?: { id: number; role: 'admin' | 'user' };
}
app.get('/users/:id', async (
req: Request<{ id: string }>,
res: Response<User>
) => {
const user: User = { id: 1, name: 'Tom' };
res.json(user);
});
// 错误处理
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
res.status(500).json({ message: err.message });
});3.2 中间件类型
typescript
import { RequestHandler } from 'express';
const authMiddleware: RequestHandler = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: '未授权' });
}
// 验证 token
next();
};
// 异步中间件
const asyncHandler =
(fn: (req: Request, res: Response) => Promise<unknown>): RequestHandler =>
(req, res, next) => {
Promise.resolve(fn(req, res)).catch(next);
};3.3 Controller 模式
typescript
class UserController {
async getById(req: Request<{ id: string }>, res: Response): Promise<void> {
try {
const id = Number(req.params.id);
const user = await userService.findById(id);
if (!user) {
res.status(404).json({ message: '用户不存在' });
return;
}
res.json(user);
} catch (err) {
res.status(500).json({ message: (err as Error).message });
}
}
async create(req: Request<{}, {}, CreateUserDTO>, res: Response): Promise<void> {
const user = await userService.create(req.body);
res.status(201).json(user);
}
}
interface CreateUserDTO {
name: string;
email: string;
password: string;
}四、ORM 中的 TS
4.1 Prisma
typescript
import { PrismaClient, User } from '@prisma/client';
const prisma = new PrismaClient();
// 自动生成的类型
async function getUser(id: number): Promise<User | null> {
return prisma.user.findUnique({ where: { id } });
}
// 包含关联
async function getUserWithPosts(id: number) {
return prisma.user.findUnique({
where: { id },
include: { posts: true }
});
}
// 类型推导
const user = await prisma.user.create({
data: {
name: 'Tom',
email: 'tom@example.com'
}
});
// user 类型: User4.2 TypeORM
typescript
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column()
name!: string;
@Column()
email!: string;
@Column({ default: false })
isActive!: boolean;
}
// Repository
const repo = dataSource.getRepository(User);
const user: User | null = await repo.findOne({ where: { id: 1 } });五、API 类型设计最佳实践
5.1 统一响应格式
typescript
// types/api.ts
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
export interface PagedData<T> {
items: T[];
total: number;
}
export interface PagedResponse<T> extends ApiResponse<PagedData<T>> {}
// 使用
async function fetchUsers(): Promise<PagedResponse<User>> {
const res = await fetch('/api/users');
return res.json();
}5.2 DTO 模式
typescript
// user.dto.ts
export interface CreateUserDTO {
name: string;
email: string;
password: string;
}
export interface UpdateUserDTO {
name?: string;
email?: string;
}
export interface LoginDTO {
email: string;
password: string;
}
export interface UserResponse {
id: number;
name: string;
email: string;
createdAt: string;
}
// 转换函数
export function toUserResponse(user: User): UserResponse {
return {
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt.toISOString()
};
}5.3 类型守卫
typescript
// 业务错误
export class BusinessError extends Error {
constructor(public code: number, message: string) {
super(message);
this.name = 'BusinessError';
}
}
// 类型守卫
export function isBusinessError(e: unknown): e is BusinessError {
return e instanceof BusinessError;
}
// 使用
try {
await createUser(data);
} catch (e) {
if (isBusinessError(e)) {
console.error(e.code, e.message); // 类型安全
} else {
throw e;
}
}六、测试中的 TS
6.1 Jest
typescript
import { describe, it, expect, vi } from 'vitest';
describe('UserService', () => {
it('应该创建用户', async () => {
const user = await userService.create({
name: 'Tom',
email: 'tom@example.com'
});
expect(user.id).toBeGreaterThan(0);
expect(user.name).toBe('Tom');
});
it('应该处理重复邮箱', async () => {
await expect(
userService.create({ name: 'B', email: 'tom@example.com' })
).rejects.toThrow('邮箱已存在');
});
});
// Mock 类型
vi.mock('@/api/user', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Tom' })
}));6.2 类型化的 Mock
typescript
import { vi } from 'vitest';
import type { UserService } from './user-service';
const mockUserService: vi.Mocked<UserService> = {
create: vi.fn(),
findById: vi.fn(),
update: vi.fn(),
delete: vi.fn()
};七、常见错误
7.1 React:children 缺失类型
typescript
// ❌ children 没声明
interface Props {
title: string;
}
function Card({ title }: Props) {
return <div>{title}</div>; // children 不能传
}
// ✅ 加上 children
interface Props {
title: string;
children?: ReactNode;
}7.2 Vue:emits 没类型
vue
<!-- ❌ 不安全 -->
<script setup>
const emit = defineEmits(['select']);
emit('select', 'invalid'); // 无类型检查
</script>
<!-- ✅ 类型化 -->
<script setup lang="ts">
const emit = defineEmits<{
select: [id: number];
}>();
</script>7.3 Express:req.body 无类型
typescript
// ❌ req.body 是 any
app.post('/users', (req, res) => {
console.log(req.body.name); // 不安全
});
// ✅ 用泛型
app.post<{}, {}, CreateUserDTO>('/users', (req, res) => {
console.log(req.body.name); // ✅
});八、企业级 TS 规范
8.1 ESLint + TS
json
// .eslintrc.json
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
}
}8.2 命名规范
typescript
// 类型/接口:PascalCase
interface UserProfile {}
type Status = 'active' | 'inactive';
// 泛型参数:T、U、V 或语义化 TKey、TValue
type Pair<T, U> = { first: T; second: U };
// 枚举:PascalCase
enum OrderStatus { Pending, Paid }
// 常量:UPPER_SNAKE_CASE
const MAX_COUNT = 100;8.3 文件组织
src/
├── types/ # 全局类型
│ ├── user.ts
│ ├── api.ts
│ └── index.ts # 统一导出
├── api/ # API 客户端
├── components/ # 组件
├── hooks/ # 自定义 hooks
├── utils/ # 工具函数
└── pages/ # 页面8.4 类型导入规范
typescript
// ✅ 用 import type 明确导入类型
import type { User } from '@/types';
import { fetchUser } from '@/api/user';
// ✅ 混合导入
import { type User, fetchUser } from '@/api/user';九、本章小结
| 框架 | 关键点 |
|---|---|
| React | props、useState、useRef、Event 类型 |
| Vue 3 | defineComponent、defineProps、emits 类型 |
| Node/Express | Request/Response 泛型、中间件类型 |
| ORM | Prisma 自动生成、TypeORM 装饰器 |
| API | 统一 ApiResponse、DTO 模式 |
| 测试 | vi.Mocked、类型守卫 |
| 规范 | ESLint + 命名 + 文件组织 |
动手练习
- React 组件:写一个泛型
<Table<T>>组件 - Vue 组件:用
<script setup lang="ts">写一个表单组件 - Express API:实现用户 CRUD,正确标注所有类型
- 统一响应:定义一套
ApiResponse<T>+ 实现若干 API
推荐阅读
- 📖 React TypeScript Cheatsheet — React + TS 速查
- 📖 Vue 3 TypeScript — Vue 3 TS 官方指南
- 📖 TypeScript Node Starter — Node + TS 模板
TypeScript 基础篇完结 🎉
接下来我们将进入 Vue 3 实战 篇章,敬请期待。