TypeScript 实战项目
更新: 8/21/2025 字数: 0 字 时长: 0 分钟
通过实际项目案例,学习如何在真实场景中应用 TypeScript,包括项目配置、架构设计和最佳实践。
项目一:Todo 应用
项目结构
todo-app/
├── src/
│ ├── types/
│ │ └── index.ts
│ ├── models/
│ │ └── Todo.ts
│ ├── services/
│ │ └── TodoService.ts
│ ├── utils/
│ │ └── storage.ts
│ ├── components/
│ │ ├── TodoItem.ts
│ │ ├── TodoList.ts
│ │ └── TodoForm.ts
│ └── main.ts
├── public/
│ └── index.html
├── package.json
└── tsconfig.json
类型定义
typescript
// src/types/index.ts
export interface Todo {
id: string;
title: string;
description?: string;
completed: boolean;
createdAt: Date;
updatedAt: Date;
priority: Priority;
tags: string[];
}
export enum Priority {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high'
}
export interface CreateTodoRequest {
title: string;
description?: string;
priority?: Priority;
tags?: string[];
}
export interface UpdateTodoRequest {
title?: string;
description?: string;
completed?: boolean;
priority?: Priority;
tags?: string[];
}
export interface TodoFilter {
completed?: boolean;
priority?: Priority;
tags?: string[];
search?: string;
}
export interface TodoStats {
total: number;
completed: number;
pending: number;
byPriority: Record<Priority, number>;
}
数据模型
typescript
// src/models/Todo.ts
import { Todo, Priority, CreateTodoRequest, UpdateTodoRequest } from '../types';
export class TodoModel implements Todo {
id: string;
title: string;
description?: string;
completed: boolean;
createdAt: Date;
updatedAt: Date;
priority: Priority;
tags: string[];
constructor(data: CreateTodoRequest) {
this.id = this.generateId();
this.title = data.title;
this.description = data.description;
this.completed = false;
this.createdAt = new Date();
this.updatedAt = new Date();
this.priority = data.priority || Priority.MEDIUM;
this.tags = data.tags || [];
}
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
update(data: UpdateTodoRequest): void {
if (data.title !== undefined) this.title = data.title;
if (data.description !== undefined) this.description = data.description;
if (data.completed !== undefined) this.completed = data.completed;
if (data.priority !== undefined) this.priority = data.priority;
if (data.tags !== undefined) this.tags = data.tags;
this.updatedAt = new Date();
}
toggle(): void {
this.completed = !this.completed;
this.updatedAt = new Date();
}
addTag(tag: string): void {
if (!this.tags.includes(tag)) {
this.tags.push(tag);
this.updatedAt = new Date();
}
}
removeTag(tag: string): void {
const index = this.tags.indexOf(tag);
if (index > -1) {
this.tags.splice(index, 1);
this.updatedAt = new Date();
}
}
toJSON(): Todo {
return {
id: this.id,
title: this.title,
description: this.description,
completed: this.completed,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
priority: this.priority,
tags: [...this.tags]
};
}
static fromJSON(data: Todo): TodoModel {
const todo = Object.create(TodoModel.prototype);
Object.assign(todo, {
...data,
createdAt: new Date(data.createdAt),
updatedAt: new Date(data.updatedAt)
});
return todo;
}
}
服务层
typescript
// src/services/TodoService.ts
import { TodoModel } from '../models/Todo';
import { Todo, CreateTodoRequest, UpdateTodoRequest, TodoFilter, TodoStats, Priority } from '../types';
import { StorageService } from '../utils/storage';
export class TodoService {
private todos: TodoModel[] = [];
private storage: StorageService<Todo[]>;
constructor() {
this.storage = new StorageService('todos');
this.loadTodos();
}
private loadTodos(): void {
const savedTodos = this.storage.get();
if (savedTodos) {
this.todos = savedTodos.map(todo => TodoModel.fromJSON(todo));
}
}
private saveTodos(): void {
this.storage.set(this.todos.map(todo => todo.toJSON()));
}
create(data: CreateTodoRequest): TodoModel {
const todo = new TodoModel(data);
this.todos.push(todo);
this.saveTodos();
return todo;
}
getAll(): TodoModel[] {
return [...this.todos];
}
getById(id: string): TodoModel | undefined {
return this.todos.find(todo => todo.id === id);
}
update(id: string, data: UpdateTodoRequest): TodoModel | null {
const todo = this.getById(id);
if (!todo) return null;
todo.update(data);
this.saveTodos();
return todo;
}
delete(id: string): boolean {
const index = this.todos.findIndex(todo => todo.id === id);
if (index === -1) return false;
this.todos.splice(index, 1);
this.saveTodos();
return true;
}
toggle(id: string): TodoModel | null {
const todo = this.getById(id);
if (!todo) return null;
todo.toggle();
this.saveTodos();
return todo;
}
filter(filter: TodoFilter): TodoModel[] {
return this.todos.filter(todo => {
if (filter.completed !== undefined && todo.completed !== filter.completed) {
return false;
}
if (filter.priority && todo.priority !== filter.priority) {
return false;
}
if (filter.tags && filter.tags.length > 0) {
const hasAllTags = filter.tags.every(tag => todo.tags.includes(tag));
if (!hasAllTags) return false;
}
if (filter.search) {
const searchLower = filter.search.toLowerCase();
const titleMatch = todo.title.toLowerCase().includes(searchLower);
const descriptionMatch = todo.description?.toLowerCase().includes(searchLower) || false;
if (!titleMatch && !descriptionMatch) return false;
}
return true;
});
}
getStats(): TodoStats {
const total = this.todos.length;
const completed = this.todos.filter(todo => todo.completed).length;
const pending = total - completed;
const byPriority = this.todos.reduce((acc, todo) => {
acc[todo.priority] = (acc[todo.priority] || 0) + 1;
return acc;
}, {} as Record<Priority, number>);
// 确保所有优先级都有值
Object.values(Priority).forEach(priority => {
if (!(priority in byPriority)) {
byPriority[priority] = 0;
}
});
return {
total,
completed,
pending,
byPriority
};
}
getAllTags(): string[] {
const tagSet = new Set<string>();
this.todos.forEach(todo => {
todo.tags.forEach(tag => tagSet.add(tag));
});
return Array.from(tagSet).sort();
}
clearCompleted(): number {
const completedCount = this.todos.filter(todo => todo.completed).length;
this.todos = this.todos.filter(todo => !todo.completed);
this.saveTodos();
return completedCount;
}
}
工具类
typescript
// src/utils/storage.ts
export class StorageService<T> {
constructor(private key: string) {}
get(): T | null {
try {
const item = localStorage.getItem(this.key);
return item ? JSON.parse(item) : null;
} catch (error) {
console.error(`Error reading from localStorage:`, error);
return null;
}
}
set(value: T): void {
try {
localStorage.setItem(this.key, JSON.stringify(value));
} catch (error) {
console.error(`Error writing to localStorage:`, error);
}
}
remove(): void {
try {
localStorage.removeItem(this.key);
} catch (error) {
console.error(`Error removing from localStorage:`, error);
}
}
clear(): void {
try {
localStorage.clear();
} catch (error) {
console.error(`Error clearing localStorage:`, error);
}
}
}
// 日期工具函数
export class DateUtils {
static formatDate(date: Date): string {
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
static formatDateTime(date: Date): string {
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
static getRelativeTime(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffMinutes = Math.floor(diffMs / (1000 * 60));
if (diffDays > 0) {
return `${diffDays}天前`;
} else if (diffHours > 0) {
return `${diffHours}小时前`;
} else if (diffMinutes > 0) {
return `${diffMinutes}分钟前`;
} else {
return '刚刚';
}
}
}
// 验证工具
export class ValidationUtils {
static isValidTodoTitle(title: string): boolean {
return title.trim().length > 0 && title.length <= 100;
}
static isValidTodoDescription(description: string): boolean {
return description.length <= 500;
}
static isValidTag(tag: string): boolean {
return /^[a-zA-Z0-9\u4e00-\u9fa5_-]+$/.test(tag) && tag.length <= 20;
}
static sanitizeInput(input: string): string {
return input.trim().replace(/[<>"'&]/g, '');
}
}
项目二:API 客户端库
类型定义
typescript
// src/types/api.ts
export interface ApiResponse<T = any> {
success: boolean;
data: T;
message: string;
timestamp: number;
errors?: string[];
}
export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, any>;
}
export interface RequestConfig {
timeout?: number;
retries?: number;
retryDelay?: number;
headers?: Record<string, string>;
params?: Record<string, any>;
}
export interface ApiClientConfig {
baseURL: string;
timeout?: number;
retries?: number;
retryDelay?: number;
defaultHeaders?: Record<string, string>;
interceptors?: {
request?: RequestInterceptor[];
response?: ResponseInterceptor[];
};
}
export type RequestInterceptor = (config: RequestConfig) => RequestConfig | Promise<RequestConfig>;
export type ResponseInterceptor = <T>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>;
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
HTTP 客户端
typescript
// src/http/HttpClient.ts
import { ApiResponse, ApiError, RequestConfig, HttpMethod } from '../types/api';
export class HttpClient {
private baseURL: string;
private defaultConfig: RequestConfig;
constructor(baseURL: string, defaultConfig: RequestConfig = {}) {
this.baseURL = baseURL.replace(/\/$/, '');
this.defaultConfig = {
timeout: 10000,
retries: 3,
retryDelay: 1000,
headers: {
'Content-Type': 'application/json',
},
...defaultConfig
};
}
async request<T = any>(
method: HttpMethod,
url: string,
data?: any,
config: RequestConfig = {}
): Promise<ApiResponse<T>> {
const mergedConfig = this.mergeConfig(config);
const fullUrl = this.buildUrl(url, mergedConfig.params);
let attempt = 0;
const maxAttempts = (mergedConfig.retries || 0) + 1;
while (attempt < maxAttempts) {
try {
const response = await this.executeRequest<T>(method, fullUrl, data, mergedConfig);
return response;
} catch (error) {
attempt++;
if (attempt >= maxAttempts || !this.shouldRetry(error)) {
throw error;
}
await this.delay(mergedConfig.retryDelay || 1000);
}
}
throw new Error('Max retries exceeded');
}
private async executeRequest<T>(
method: HttpMethod,
url: string,
data: any,
config: RequestConfig
): Promise<ApiResponse<T>> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), config.timeout);
try {
const fetchConfig: RequestInit = {
method,
headers: config.headers,
signal: controller.signal
};
if (data && ['POST', 'PUT', 'PATCH'].includes(method)) {
fetchConfig.body = JSON.stringify(data);
}
const response = await fetch(url, fetchConfig);
clearTimeout(timeoutId);
if (!response.ok) {
throw await this.createApiError(response);
}
const result = await response.json();
return result;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new ApiError('TIMEOUT', 'Request timeout');
}
throw error;
}
}
private mergeConfig(config: RequestConfig): RequestConfig {
return {
...this.defaultConfig,
...config,
headers: {
...this.defaultConfig.headers,
...config.headers
}
};
}
private buildUrl(url: string, params?: Record<string, any>): string {
const fullUrl = url.startsWith('http') ? url : `${this.baseURL}${url}`;
if (!params || Object.keys(params).length === 0) {
return fullUrl;
}
const urlObj = new URL(fullUrl);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
urlObj.searchParams.append(key, String(value));
}
});
return urlObj.toString();
}
private async createApiError(response: Response): Promise<ApiError> {
try {
const errorData = await response.json();
return new ApiError(
errorData.code || `HTTP_${response.status}`,
errorData.message || response.statusText,
errorData.details
);
} catch {
return new ApiError(
`HTTP_${response.status}`,
response.statusText
);
}
}
private shouldRetry(error: any): boolean {
if (error instanceof ApiError) {
// 不重试客户端错误
return !error.code.startsWith('HTTP_4');
}
return true;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 便捷方法
get<T = any>(url: string, config?: RequestConfig): Promise<ApiResponse<T>> {
return this.request<T>('GET', url, undefined, config);
}
post<T = any>(url: string, data?: any, config?: RequestConfig): Promise<ApiResponse<T>> {
return this.request<T>('POST', url, data, config);
}
put<T = any>(url: string, data?: any, config?: RequestConfig): Promise<ApiResponse<T>> {
return this.request<T>('PUT', url, data, config);
}
delete<T = any>(url: string, config?: RequestConfig): Promise<ApiResponse<T>> {
return this.request<T>('DELETE', url, undefined, config);
}
patch<T = any>(url: string, data?: any, config?: RequestConfig): Promise<ApiResponse<T>> {
return this.request<T>('PATCH', url, data, config);
}
}
class ApiError extends Error {
constructor(
public code: string,
message: string,
public details?: Record<string, any>
) {
super(message);
this.name = 'ApiError';
}
}
API 客户端
typescript
// src/ApiClient.ts
import { HttpClient } from './http/HttpClient';
import {
ApiClientConfig,
ApiResponse,
PaginatedResponse,
RequestInterceptor,
ResponseInterceptor
} from './types/api';
export class ApiClient {
private httpClient: HttpClient;
private requestInterceptors: RequestInterceptor[] = [];
private responseInterceptors: ResponseInterceptor[] = [];
constructor(config: ApiClientConfig) {
this.httpClient = new HttpClient(config.baseURL, {
timeout: config.timeout,
retries: config.retries,
retryDelay: config.retryDelay,
headers: config.defaultHeaders
});
if (config.interceptors) {
this.requestInterceptors = config.interceptors.request || [];
this.responseInterceptors = config.interceptors.response || [];
}
}
// 拦截器管理
addRequestInterceptor(interceptor: RequestInterceptor): void {
this.requestInterceptors.push(interceptor);
}
addResponseInterceptor(interceptor: ResponseInterceptor): void {
this.responseInterceptors.push(interceptor);
}
// 资源操作方法
async get<T>(url: string, params?: Record<string, any>): Promise<T> {
const response = await this.httpClient.get<T>(url, { params });
return this.processResponse(response);
}
async post<T>(url: string, data?: any): Promise<T> {
const response = await this.httpClient.post<T>(url, data);
return this.processResponse(response);
}
async put<T>(url: string, data?: any): Promise<T> {
const response = await this.httpClient.put<T>(url, data);
return this.processResponse(response);
}
async delete<T>(url: string): Promise<T> {
const response = await this.httpClient.delete<T>(url);
return this.processResponse(response);
}
async patch<T>(url: string, data?: any): Promise<T> {
const response = await this.httpClient.patch<T>(url, data);
return this.processResponse(response);
}
// 分页查询
async getPaginated<T>(
url: string,
page: number = 1,
limit: number = 10,
params?: Record<string, any>
): Promise<PaginatedResponse<T>> {
const response = await this.httpClient.get<T[]>(url, {
params: { page, limit, ...params }
});
return response as PaginatedResponse<T>;
}
// 批量操作
async batchGet<T>(urls: string[]): Promise<T[]> {
const promises = urls.map(url => this.get<T>(url));
return Promise.all(promises);
}
async batchPost<T>(requests: Array<{ url: string; data: any }>): Promise<T[]> {
const promises = requests.map(req => this.post<T>(req.url, req.data));
return Promise.all(promises);
}
// 文件上传
async uploadFile<T>(
url: string,
file: File,
onProgress?: (progress: number) => void
): Promise<T> {
const formData = new FormData();
formData.append('file', file);
// 这里简化实现,实际项目中需要处理上传进度
const response = await this.httpClient.post<T>(url, formData);
return this.processResponse(response);
}
private async processResponse<T>(response: ApiResponse<T>): Promise<T> {
// 应用响应拦截器
let processedResponse = response;
for (const interceptor of this.responseInterceptors) {
processedResponse = await interceptor(processedResponse);
}
if (!processedResponse.success) {
throw new Error(processedResponse.message || 'API request failed');
}
return processedResponse.data;
}
}
// 创建具体的 API 服务
export class UserApiService {
constructor(private apiClient: ApiClient) {}
async getUsers(page?: number, limit?: number) {
return this.apiClient.getPaginated<User>('/users', page, limit);
}
async getUser(id: number): Promise<User> {
return this.apiClient.get<User>(`/users/${id}`);
}
async createUser(userData: CreateUserRequest): Promise<User> {
return this.apiClient.post<User>('/users', userData);
}
async updateUser(id: number, userData: UpdateUserRequest): Promise<User> {
return this.apiClient.put<User>(`/users/${id}`, userData);
}
async deleteUser(id: number): Promise<void> {
return this.apiClient.delete<void>(`/users/${id}`);
}
async searchUsers(query: string): Promise<User[]> {
return this.apiClient.get<User[]>('/users/search', { q: query });
}
}
// 使用示例
interface User {
id: number;
name: string;
email: string;
createdAt: string;
}
interface CreateUserRequest {
name: string;
email: string;
}
interface UpdateUserRequest {
name?: string;
email?: string;
}
// 创建 API 客户端实例
const apiClient = new ApiClient({
baseURL: 'https://api.example.com',
timeout: 10000,
defaultHeaders: {
'Authorization': 'Bearer your-token-here'
},
interceptors: {
request: [
// 添加认证 token
async (config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers = {
...config.headers,
'Authorization': `Bearer ${token}`
};
}
return config;
}
],
response: [
// 处理认证失败
async (response) => {
if (!response.success && response.message.includes('Unauthorized')) {
// 重定向到登录页面
window.location.href = '/login';
}
return response;
}
]
}
});
const userService = new UserApiService(apiClient);
// 使用服务
async function example() {
try {
const users = await userService.getUsers(1, 10);
console.log('Users:', users.data);
console.log('Pagination:', users.pagination);
const newUser = await userService.createUser({
name: 'John Doe',
email: 'john@example.com'
});
console.log('Created user:', newUser);
} catch (error) {
console.error('API Error:', error);
}
}
项目配置
tsconfig.json
json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"allowJs": true,
"checkJs": false,
"outDir": "./dist",
"rootDir": "./src",
"removeComments": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@types/*": ["src/types/*"],
"@utils/*": ["src/utils/*"],
"@services/*": ["src/services/*"],
"@models/*": ["src/models/*"]
}
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}
package.json
json
{
"name": "typescript-projects",
"version": "1.0.0",
"description": "TypeScript实战项目集合",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"build:watch": "tsc --watch",
"dev": "ts-node src/main.ts",
"start": "node dist/main.js",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"format": "prettier --write src/**/*.ts",
"type-check": "tsc --noEmit",
"clean": "rimraf dist",
"prebuild": "npm run clean"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/jest": "^29.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.0.0",
"jest": "^29.0.0",
"prettier": "^3.0.0",
"rimraf": "^5.0.0",
"ts-jest": "^29.0.0",
"ts-node": "^10.0.0",
"typescript": "^5.0.0"
},
"dependencies": {
"reflect-metadata": "^0.1.13"
}
}
ESLint 配置
json
// .eslintrc.json
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module",
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"@typescript-eslint/recommended",
"@typescript-eslint/recommended-requiring-type-checking"
],
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/prefer-const": "error",
"@typescript-eslint/no-non-null-assertion": "warn"
},
"env": {
"node": true,
"browser": true,
"es2020": true
}
}
测试配置
Jest 配置
json
// jest.config.json
{
"preset": "ts-jest",
"testEnvironment": "node",
"roots": ["<rootDir>/src"],
"testMatch": [
"**/__tests__/**/*.ts",
"**/?(*.)+(spec|test).ts"
],
"transform": {
"^.+\.ts$": "ts-jest"
},
"collectCoverageFrom": [
"src/**/*.ts",
"!src/**/*.d.ts",
"!src/**/*.test.ts",
"!src/**/*.spec.ts"
],
"coverageDirectory": "coverage",
"coverageReporters": ["text", "lcov", "html"]
}
测试示例
typescript
// src/__tests__/TodoService.test.ts
import { TodoService } from '../services/TodoService';
import { Priority } from '../types';
// Mock localStorage
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn(),
};
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
});
describe('TodoService', () => {
let todoService: TodoService;
beforeEach(() => {
localStorageMock.getItem.mockReturnValue(null);
localStorageMock.setItem.mockClear();
todoService = new TodoService();
});
describe('create', () => {
it('should create a new todo', () => {
const todoData = {
title: 'Test Todo',
description: 'Test Description',
priority: Priority.HIGH
};
const todo = todoService.create(todoData);
expect(todo.title).toBe(todoData.title);
expect(todo.description).toBe(todoData.description);
expect(todo.priority).toBe(todoData.priority);
expect(todo.completed).toBe(false);
expect(todo.id).toBeDefined();
expect(localStorageMock.setItem).toHaveBeenCalled();
});
it('should set default priority to MEDIUM', () => {
const todo = todoService.create({ title: 'Test Todo' });
expect(todo.priority).toBe(Priority.MEDIUM);
});
});
describe('getAll', () => {
it('should return all todos', () => {
todoService.create({ title: 'Todo 1' });
todoService.create({ title: 'Todo 2' });
const todos = todoService.getAll();
expect(todos).toHaveLength(2);
});
});
describe('filter', () => {
beforeEach(() => {
todoService.create({ title: 'Completed Todo', priority: Priority.HIGH });
todoService.create({ title: 'Pending Todo', priority: Priority.LOW });
const todos = todoService.getAll();
todoService.toggle(todos[0].id); // 完成第一个
});
it('should filter by completed status', () => {
const completedTodos = todoService.filter({ completed: true });
const pendingTodos = todoService.filter({ completed: false });
expect(completedTodos).toHaveLength(1);
expect(pendingTodos).toHaveLength(1);
expect(completedTodos[0].completed).toBe(true);
expect(pendingTodos[0].completed).toBe(false);
});
it('should filter by priority', () => {
const highPriorityTodos = todoService.filter({ priority: Priority.HIGH });
const lowPriorityTodos = todoService.filter({ priority: Priority.LOW });
expect(highPriorityTodos).toHaveLength(1);
expect(lowPriorityTodos).toHaveLength(1);
});
it('should filter by search term', () => {
const searchResults = todoService.filter({ search: 'Completed' });
expect(searchResults).toHaveLength(1);
expect(searchResults[0].title).toContain('Completed');
});
});
describe('getStats', () => {
it('should return correct statistics', () => {
todoService.create({ title: 'Todo 1', priority: Priority.HIGH });
todoService.create({ title: 'Todo 2', priority: Priority.LOW });
const todos = todoService.getAll();
todoService.toggle(todos[0].id);
const stats = todoService.getStats();
expect(stats.total).toBe(2);
expect(stats.completed).toBe(1);
expect(stats.pending).toBe(1);
expect(stats.byPriority[Priority.HIGH]).toBe(1);
expect(stats.byPriority[Priority.LOW]).toBe(1);
expect(stats.byPriority[Priority.MEDIUM]).toBe(0);
});
});
});
总结
通过这些实战项目,我们学习了:
项目架构:
- 分层架构:类型定义、模型、服务、工具分离
- 模块化设计:清晰的模块边界和依赖关系
- 类型安全:全程类型检查和类型推导
开发实践:
- 配置管理:TypeScript、ESLint、Jest 配置
- 代码质量:静态检查、单元测试、代码覆盖率
- 工程化:构建脚本、开发工具链
TypeScript 特性应用:
- 接口设计:API 响应、配置选项、业务模型
- 泛型应用:通用工具类、API 客户端
- 高级类型:条件类型、映射类型、工具类型
- 装饰器使用:元数据、验证、日志
这些项目展示了 TypeScript 在实际开发中的强大能力,帮助我们构建类型安全、可维护的应用程序。