Skip to content

TypeScript + React实战项目结构

8. 项目文件组织

my-app/
├── src/
│   ├── types/                    # 全局类型定义
│   │   ├── index.ts
│   │   ├── api.ts               # API 相关类型
│   │   └── models.ts            # 数据模型类型
│   ├── components/              # React 组件
│   │   ├── common/              # 通用组件
│   │   │   ├── Button.tsx
│   │   │   ├── Input.tsx
│   │   │   └── Modal.tsx
│   │   ├── layout/              # 布局组件
│   │   │   ├── Header.tsx
│   │   │   ├── Sidebar.tsx
│   │   │   └── Footer.tsx
│   │   └── pages/               # 页面组件
│   │       ├── Home.tsx
│   │       ├── UserList.tsx
│   │       └── UserDetail.tsx
│   ├── hooks/                   # 自定义 Hooks
│   │   ├── useForm.ts
│   │   ├── useFetch.ts
│   │   └── useAuth.ts
│   ├── services/                # API 服务
│   │   ├── api.ts
│   │   ├── userService.ts
│   │   └── authService.ts
│   ├── store/                   # 状态管理
│   │   ├── index.ts
│   │   ├── slices/
│   │   │   ├── userSlice.ts
│   │   │   └── appSlice.ts
│   │   └── selectors.ts
│   ├── utils/                   # 工具函数
│   │   ├── helpers.ts
│   │   ├── formatters.ts
│   │   └── validators.ts
│   ├── styles/                  # 样式文件
│   │   ├── global.css
│   │   └── variables.css
│   ├── App.tsx
│   └── index.tsx
├── tsconfig.json
└── package.json

9. 类型定义最佳实践

9.1 API 响应类型

typescript
// types/api.ts

// 通用 API 响应格式
export interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

// 分页响应
export interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
}

// 错误响应
export interface ErrorResponse {
  code: number;
  message: string;
  errors?: Record<string, string>;
}

9.2 数据模型类型

typescript
// types/models.ts

export interface User {
  id: number;
  name: string;
  email: string;
  avatar?: string;
  role: "admin" | "user";
  status: "active" | "inactive";
  createdAt: string;
  updatedAt: string;
}

export interface UserForm {
  name: string;
  email: string;
  role: "admin" | "user";
}

// 类型转换函数
export function toUserForm(user: User): UserForm {
  return {
    name: user.name,
    email: user.email,
    role: user.role,
  };
}

10. API 服务层

typescript
// services/api.ts

import { ApiResponse, User, PaginatedResponse } from "../types";

const API_BASE = "https://api.example.com";

async function request<T>(endpoint: string, options?: RequestInit): Promise<T> {
  const response = await fetch(`${API_BASE}${endpoint}`, {
    headers: {
      "Content-Type": "application/json",
      ...options?.headers,
    },
    ...options,
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  return response.json();
}

// services/userService.ts
export async function getUsers(
  page: number = 1,
  pageSize: number = 10,
): Promise<ApiResponse<PaginatedResponse<User>>> {
  return request("/users", {
    method: "GET",
    headers: { "X-Page": String(page), "X-PageSize": String(pageSize) },
  });
}

export async function getUser(id: number): Promise<ApiResponse<User>> {
  return request(`/users/${id}`);
}

export async function createUser(
  data: Omit<User, "id" | "createdAt" | "updatedAt">,
): Promise<ApiResponse<User>> {
  return request("/users", {
    method: "POST",
    body: JSON.stringify(data),
  });
}

export async function updateUser(
  id: number,
  data: Partial<User>,
): Promise<ApiResponse<User>> {
  return request(`/users/${id}`, {
    method: "PUT",
    body: JSON.stringify(data),
  });
}

export async function deleteUser(id: number): Promise<ApiResponse<null>> {
  return request(`/users/${id}`, {
    method: "DELETE",
  });
}

11. Redux Toolkit + TypeScript

typescript
// store/slices/userSlice.ts

import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { User, ApiResponse, PaginatedResponse } from "../../types";
import * as userService from "../../services/userService";

interface UserState {
  users: User[];
  currentUser: User | null;
  loading: boolean;
  error: string | null;
  pagination: { page: number; pageSize: number; total: number };
}

const initialState: UserState = {
  users: [],
  currentUser: null,
  loading: false,
  error: null,
  pagination: { page: 1, pageSize: 10, total: 0 },
};

// 异步 thunk
export const fetchUsers = createAsyncThunk<
  PaginatedResponse<User>,
  { page: number; pageSize: number },
  { rejectValue: string }
>("users/fetchUsers", async ({ page, pageSize }, { rejectWithValue }) => {
  try {
    const response = await userService.getUsers(page, pageSize);
    return response.data;
  } catch (error) {
    return rejectWithValue(
      error instanceof Error ? error.message : "Failed to fetch users",
    );
  }
});

export const fetchUser = createAsyncThunk<
  User,
  number,
  { rejectValue: string }
>("users/fetchUser", async (id, { rejectWithValue }) => {
  try {
    const response = await userService.getUser(id);
    return response.data;
  } catch (error) {
    return rejectWithValue(
      error instanceof Error ? error.message : "Failed to fetch user",
    );
  }
});

export const createNewUser = createAsyncThunk<
  User,
  Omit<User, "id" | "createdAt" | "updatedAt">,
  { rejectValue: string }
>("users/createUser", async (data, { rejectWithValue }) => {
  try {
    const response = await userService.createUser(data);
    return response.data;
  } catch (error) {
    return rejectWithValue(
      error instanceof Error ? error.message : "Failed to create user",
    );
  }
});

const userSlice = createSlice({
  name: "users",
  initialState,
  reducers: {
    clearError(state) {
      state.error = null;
    },
    setCurrentUser(state, action: PayloadAction<User | null>) {
      state.currentUser = action.payload;
    },
  },
  extraReducers(builder) {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.loading = false;
        state.users = action.payload.items;
        state.pagination = {
          page: action.payload.page,
          pageSize: action.payload.pageSize,
          total: action.payload.total,
        };
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.loading = false;
        state.error = action.payload || "Unknown error";
      })
      .addCase(fetchUser.fulfilled, (state, action) => {
        state.currentUser = action.payload;
      })
      .addCase(createNewUser.fulfilled, (state, action) => {
        state.users.unshift(action.payload);
      });
  },
});

export const { clearError, setCurrentUser } = userSlice.actions;
export default userSlice.reducer;

// store/selectors.ts (选择器)
import { RootState } from "./index";

export const selectUsers = (state: RootState) => state.users.users;
export const selectCurrentUser = (state: RootState) => state.users.currentUser;
export const selectUserLoading = (state: RootState) => state.users.loading;
export const selectUserError = (state: RootState) => state.users.error;
export const selectPagination = (state: RootState) => state.users.pagination;

// store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import userReducer from "./slices/userSlice";

export const store = configureStore({
  reducer: {
    users: userReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

12. 完整页面组件示例

typescript
// components/pages/UserList.tsx

import { FC, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch } from '../../store';
import {
  fetchUsers,
  createNewUser
} from '../../store/slices/userSlice';
import {
  selectUsers,
  selectUserLoading,
  selectUserError,
  selectPagination
} from '../../store/selectors';
import { User, UserForm } from '../../types';

interface UserListProps {
  onUserClick?: (user: User) => void;
}

const UserList: FC<UserListProps> = ({ onUserClick }) => {
  const dispatch = useDispatch<AppDispatch>();
  const users = useSelector(selectUsers);
  const loading = useSelector(selectUserLoading);
  const error = useSelector(selectUserError);
  const pagination = useSelector(selectPagination);

  useEffect(() => {
    dispatch(fetchUsers({ page: pagination.page, pageSize: pagination.pageSize }));
  }, [dispatch, pagination.page, pagination.pageSize]);

  const handleCreateUser = async (formData: UserForm) => {
    await dispatch(createNewUser(formData as any));
  };

  if (loading) return <div className="loading">Loading...</div>;
  if (error) return <div className="error">Error: {error}</div>;

  return (
    <div className="user-list">
      <h1>Users</h1>

      {users.length === 0 ? (
        <div>No users found</div>
      ) : (
        <table>
          <thead>
            <tr>
              <th>Name</th>
              <th>Email</th>
              <th>Role</th>
              <th>Status</th>
              <th>Actions</th>
            </tr>
          </thead>
          <tbody>
            {users.map(user => (
              <tr key={user.id} onClick={() => onUserClick?.(user)}>
                <td>{user.name}</td>
                <td>{user.email}</td>
                <td>{user.role}</td>
                <td>{user.status}</td>
                <td>
                  <button>Edit</button>
                  <button>Delete</button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      )}

      <div className="pagination">
        <span>
          Page {pagination.page} of {Math.ceil(pagination.total / pagination.pageSize)}
        </span>
      </div>
    </div>
  );
};

export default UserList;

13. 表单组件示例

typescript
// components/UserForm.tsx

import { FC } from 'react';
import { User, UserForm as UserFormData } from '../types';
import { useForm } from '../hooks/useForm';

interface UserFormProps {
  initialData?: User;
  onSubmit: (data: UserFormData) => Promise<void>;
  loading?: boolean;
}

const UserForm: FC<UserFormProps> = ({ initialData, onSubmit, loading = false }) => {
  const form = useForm<UserFormData>({
    initialValues: {
      name: initialData?.name || '',
      email: initialData?.email || '',
      role: initialData?.role || 'user'
    },
    validate: (values) => {
      const errors: Partial<Record<keyof UserFormData, string>> = {};

      if (!values.name.trim()) {
        errors.name = 'Name is required';
      }

      if (!values.email.trim()) {
        errors.email = 'Email is required';
      } else if (!/\S+@\S+\.\S+/.test(values.email)) {
        errors.email = 'Email is invalid';
      }

      return errors;
    },
    onSubmit
  });

  return (
    <form onSubmit={form.handleSubmit}>
      <div className="form-group">
        <label htmlFor="name">Name</label>
        <input
          id="name"
          name="name"
          value={form.values.name}
          onChange={form.handleChange}
          onBlur={form.handleBlur}
          disabled={loading}
        />
        {form.touched.name && form.errors.name && (
          <span className="error">{form.errors.name}</span>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="email">Email</label>
        <input
          id="email"
          type="email"
          name="email"
          value={form.values.email}
          onChange={form.handleChange}
          onBlur={form.handleBlur}
          disabled={loading}
        />
        {form.touched.email && form.errors.email && (
          <span className="error">{form.errors.email}</span>
        )}
      </div>

      <div className="form-group">
        <label htmlFor="role">Role</label>
        <select
          id="role"
          name="role"
          value={form.values.role}
          onChange={(e) => form.handleChange(e as any)}
          disabled={loading}
        >
          <option value="user">User</option>
          <option value="admin">Admin</option>
        </select>
      </div>

      <button type="submit" disabled={loading}>
        {loading ? 'Submitting...' : 'Submit'}
      </button>
    </form>
  );
};

export default UserForm;

TypeScript + React 高级类型技巧

14. 工具类型

typescript
// Partial<T> - 所有属性可选
type UserPartial = Partial<User>;

// Required<T> - 所有属性必填
type UserRequired = Required<User>;

// Readonly<T> - 所有属性只读
type UserReadonly = Readonly<User>;

// Pick<T, K> - 选择特定属性
type UserPreview = Pick<User, "id" | "name" | "email">;

// Omit<T, K> - 排除特定属性
type UserWithoutPassword = Omit<User, "password">;

// Record<K, T> - 映射类型
type UserRoles = Record<"admin" | "user" | "guest", User[]>;

// Exclude<T, U> - 从类型中排除
type NonNullableUser = Exclude<User | null | undefined, null | undefined>;

// Extract<T, U> - 从类型中提取
type UserOrString = Extract<User | string | number, User | string>;

// ReturnType<T> - 获取函数返回类型
type UserResponse = ReturnType<typeof getUser>;

// Parameters<T> - 获取函数参数类型
type GetUserParams = Parameters<typeof getUser>;

15. 条件类型和泛型约束

typescript
// 条件类型
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<number>; // false

// 在实战中的应用
type ApiData<T> = T extends { data: infer D } ? D : T;

interface Response {
  data: User;
}

type ExtractedData = ApiData<Response>; // User

// 泛型约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = { id: 1, name: "Alice", email: "alice@example.com" };
const name = getProperty(user, "name"); // 类型正确
// getProperty(user, 'nonexistent');  // 错误!

16. 类型推断与声明

typescript
// 类型推断
const user = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
} as const; // const assertion

type UserType = typeof user; // 自动推断类型

// 从数组推断
const roles = ["admin", "user", "guest"] as const;
type Role = (typeof roles)[number]; // 'admin' | 'user' | 'guest'

// 从函数推断
function createUser(data: UserForm): User {
  return { ...data, id: 1, createdAt: "", updatedAt: "" };
}

type CreateUserReturn = ReturnType<typeof createUser>;

TypeScript + react常见模式和最佳实践

17. 错误处理

typescript
// 定义错误类型
interface ApiError extends Error {
  code: number;
  status: number;
}

class RequestError extends Error implements ApiError {
  code: number;
  status: number;

  constructor(message: string, code: number, status: number) {
    super(message);
    this.code = code;
    this.status = status;
    this.name = "RequestError";
  }
}

// 在 hook 中使用
function useSafeAsyncEffect<T>(
  effect: () => Promise<T>,
  onSuccess: (data: T) => void,
  onError?: (error: Error) => void,
): void {
  useEffect(() => {
    let isMounted = true;

    effect()
      .then((data) => {
        if (isMounted) onSuccess(data);
      })
      .catch((error) => {
        if (isMounted) {
          onError?.(error instanceof Error ? error : new Error(String(error)));
        }
      });

    return () => {
      isMounted = false;
    };
  }, [effect, onSuccess, onError]);
}

18. 组件组合与继承

typescript
// 组件组合(推荐)
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary';
  size?: 'small' | 'medium' | 'large';
  loading?: boolean;
}

const Button: FC<ButtonProps> = ({
  variant = 'primary',
  size = 'medium',
  loading = false,
  children,
  disabled,
  ...rest
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled || loading}
      {...rest}
    >
      {loading ? 'Loading...' : children}
    </button>
  );
};

// 扩展现有组件
interface PrimaryButtonProps extends ButtonProps {
  variant?: 'primary';
}

const PrimaryButton: FC<PrimaryButtonProps> = (props) => (
  <Button variant="primary" {...props} />
);

19. 配置和环境变量

typescript
// types/env.ts
declare namespace process {
  namespace env {
    const REACT_APP_API_URL: string;
    const REACT_APP_ENV: "development" | "staging" | "production";
  }
}

// config.ts
export const config = {
  apiUrl: process.env.REACT_APP_API_URL,
  env: process.env.REACT_APP_ENV,
  isDevelopment: process.env.REACT_APP_ENV === "development",
  isProduction: process.env.REACT_APP_ENV === "production",
} as const;

// 使用
fetch(`${config.apiUrl}/users`);

ts+reactjs完整实战示例

20. 用户管理系统

typescript
// App.tsx
import { FC } from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { store } from './store';
import { ThemeProvider } from './context/ThemeContext';
import Layout from './components/layout/Layout';
import UserListPage from './components/pages/UserListPage';
import UserDetailPage from './components/pages/UserDetailPage';

const App: FC = () => {
  return (
    <Provider store={store}>
      <ThemeProvider>
        <BrowserRouter>
          <Layout>
            <Routes>
              <Route path="/" element={<UserListPage />} />
              <Route path="/users/:id" element={<UserDetailPage />} />
            </Routes>
          </Layout>
        </BrowserRouter>
      </ThemeProvider>
    </Provider>
  );
};

export default App;
typescript
// components/pages/UserListPage.tsx
import { FC, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch } from '../../store';
import { fetchUsers, createNewUser } from '../../store/slices/userSlice';
import { selectUsers, selectUserLoading } from '../../store/selectors';
import UserList from '../UserList';
import UserForm from '../UserForm';
import { UserForm as UserFormData } from '../../types';

const UserListPage: FC = () => {
  const dispatch = useDispatch<AppDispatch>();
  const users = useSelector(selectUsers);
  const loading = useSelector(selectUserLoading);
  const [showForm, setShowForm] = useState(false);

  const handleCreateUser = async (data: UserFormData) => {
    await dispatch(createNewUser(data as any));
    setShowForm(false);
  };

  return (
    <div>
      <button onClick={() => setShowForm(!showForm)}>
        {showForm ? 'Cancel' : 'Add User'}
      </button>

      {showForm && (
        <UserForm onSubmit={handleCreateUser} loading={loading} />
      )}

      <UserList />
    </div>
  );
};

export default UserListPage;