Skip to content
第 110 / 250 章前端⏱ 10 分钟阅读

第 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 类型: User

4.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';

九、本章小结

框架关键点
Reactprops、useState、useRef、Event 类型
Vue 3defineComponent、defineProps、emits 类型
Node/ExpressRequest/Response 泛型、中间件类型
ORMPrisma 自动生成、TypeORM 装饰器
API统一 ApiResponse、DTO 模式
测试vi.Mocked、类型守卫
规范ESLint + 命名 + 文件组织

动手练习

  1. React 组件:写一个泛型 <Table<T>> 组件
  2. Vue 组件:用 <script setup lang="ts"> 写一个表单组件
  3. Express API:实现用户 CRUD,正确标注所有类型
  4. 统一响应:定义一套 ApiResponse<T> + 实现若干 API

推荐阅读


TypeScript 基础篇完结 🎉

接下来我们将进入 Vue 3 实战 篇章,敬请期待。

本站基于 VitePress 构建 · 由 Codebook 团队维护