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.json9. 类型定义最佳实践
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;