Правила написания кода
Проект следует строгим правилам написания кода для обеспечения качества, читаемости и консистентности.
Основные принципы
1. Return типы функций
НИКОГДА не пишем explicit return type для функций. TypeScript сам выведет тип.
// ❌ Неправильно
function getValue(): string {
return "hello";
}
const add = (a: number, b: number): number => a + b;
// ✅ Правильно
function getValue() {
return "hello";
}
const add = (a: number, b: number) => a + b;2. Early Return Pattern
Используем early return вместо вложенных условий.
// ❌ Неправильно
function processUser(user: IUser | null) {
if (user) {
if (user.isActive) {
// 100 строк логики...
}
}
}
// ✅ Правильно
function processUser(user: IUser | null) {
if (!user) return;
if (!user.isActive) return;
// 100 строк логики...
}3. Утилиты
Если утилита НЕ зависит от DI или environment, выносим в отдельный файл.
// ✅ Структура
// utils/format-date.util.ts
export function formatDate(date: Date): string {
return date.toLocaleDateString();
}
// Использование в компоненте
import { formatDate } from "../utils/format-date.util";
const formatted = formatDate(new Date());4. Параметры функций
Если функция принимает больше 2 параметров, используем объект с интерфейсом.
// ❌ Неправильно
function create(name: string, age: number, email: string, city: string) {}
// ✅ Правильно
interface CreateUserData {
name: string;
age: number;
email: string;
city: string;
}
function create(data: CreateUserData) {}
// Использование
create({
name: "John",
age: 30,
email: "john@example.com",
city: "NYC"
});5. Интерфейсы
Интерфейсы ВСЕГДА выносим в отдельные файлы.
// ✅ Правильная структура
// interfaces/user.interface.ts
export interface IUser {
id: string;
email: string;
firstName?: string;
lastName?: string;
}
// services/user.service.ts
import { IUser } from "../interfaces/user.interface";
@Injectable({ providedIn: "root" })
export class UserService {
getUser(): Observable<IUser> {
/* ... */
}
}Правила именования:
- Всегда с префиксом
I:IUser,ICreateRequest,IApiResponse - Именование в PascalCase
- Один интерфейс на файл (основной интерфейс)
6. Именование переменных
Используем краткие понятные версии.
// ❌ Неправильно
const mes = "message";
const m = [1, 2, 3];
const messag = "hello";
const messageEntity = message;
// ✅ Правильно
const message = "message";
const items = [1, 2, 3];
const text = "hello";
const messageIntegration = message;Архитектура
7. Модульная структура
Используем структуру с разделением по сущностям.
// ✅ Структура
companies/
├── interfaces/
│ └── company.interface.ts
├── services/
│ └── companies.service.ts
├── components/
│ └── company-list.component.ts
├── pages/
│ └── companies-page.component.ts
└── companies.routes.ts8. Signals & toSignal для компонентов
ВСЕГДА используем toSignal() для преобразования Observables в Signals:
// ❌ Старый стиль (OnInit + subscribe)
@Component({...})
export class MyComponent implements OnInit {
data: any;
loading = true;
ngOnInit() {
this.service.getData().subscribe(d => {
this.data = d;
this.loading = false;
});
}
}
// ✅ Новый стиль (toSignal)
@Component({...}, changeDetection: ChangeDetectionStrategy.OnPush)
export class MyComponent {
private readonly data$ = this.service.getData();
readonly data = toSignal(this.data$, { initialValue: [] });
readonly loading = toSignal(
this.data$.pipe(map(() => false)),
{ initialValue: true }
);
}Правила:
- Убираем
implements OnInitиngOnInit() - Используем
toSignal(observable$, { initialValue }) - Добавляем
ChangeDetectionStrategy.OnPush - В шаблонах вызываем сигналы как функции:
data()вместоdata$ | async - Все состояние в сигналах (loading, error, data)
9. Комментарии
НЕ пишем комментарии к коду. Код должен быть самодокументируемым.
// ❌ Неправильно
// Get user by ID and return it
function getUser(id: string) {
// Find user in the list
return users.find((u) => u.id === id);
}
// ✅ Правильно
function getUserById(id: string) {
return users.find((user) => user.id === id);
}9. Декомпозиция
НЕ выносим функцию, если она вызывается один раз. Исключение — утилиты.
// ❌ Неправильно
function validateEmail(email: string) {
return email.includes("@");
}
function handleSubmit(data: IFormData) {
if (!validateEmail(data.email)) {
showError("Invalid email");
}
}
// ✅ Правильно
function handleSubmit(data: IFormData) {
if (!data.email.includes("@")) {
showError("Invalid email");
}
}
// ✅ Исключение — общая утилита
// utils/validate-email.util.ts
export function isValidEmail(email: string): boolean {
return email.includes("@");
}UX/Интеграция
10. Индикаторы загрузки и уведомления
НИКОГДА не добавляем локальные Loading Spinner / mat-spinner в компоненты.
Глобальный лоадер
Используем GlobalProgressBarComponent — полоса вверху страницы:
// Автоматический loading для POST/PUT/DELETE через LoadingService
this.service.save(data).pipe(this.loadingService.withLoading()).subscribe();Toast уведомления
Используем ToastrService для показа сообщений:
// Через RxJS оператор
this.service
.save(data)
.pipe(
this.loadingService.withLoading(),
this.toastrService.withToastr({
success: "Данные сохранены",
error: "Ошибка при сохранении"
})
)
.subscribe();
// Ручной вызов
this.toastrService.success("Успешно!");
this.toastrService.error("Ошибка");
this.toastrService.warning("Внимание");
this.toastrService.info("Информация");Минимализм
11. Объём кода
Идеальный сервис должен умещаться на один экран монитора (50-80 строк).
// ✅ Правильный размер сервиса
@Injectable({ providedIn: "root" })
export class UserService {
private readonly api = inject(ApiService);
getAll() {
return this.api.get<IUser[]>("/users");
}
getById(id: string) {
return this.api.get<IUser>(`/users/${id}`);
}
create(data: ICreateUserRequest) {
return this.api.post<IUser>("/users", data);
}
update(id: string, data: IUpdateUserRequest) {
return this.api.put<IUser>(`/users/${id}`, data);
}
delete(id: string) {
return this.api.delete<null>(`/users/${id}`);
}
}12. Dependency Injection
ВСЕ зависимости объявляем через inject() (functional API).
// ✅ Правильно (Functional API)
@Injectable({ providedIn: "root" })
export class UserService {
private readonly api = inject(ApiService);
private readonly router = inject(Router);
logout() {
this.router.navigate(["/login"]);
}
}
// ❌ Старый стиль (конструктор)
@Injectable({ providedIn: "root" })
export class UserService {
constructor(private api: ApiService) {}
}13. Private Readonly
Все зависимости и локальные переменные с private readonly и префиксом _:
// ✅ Правильно
@Injectable({ providedIn: "root" })
export class LoggerService {
private readonly _logger = new Logger(LoggerService.name);
private readonly _cache = inject(CacheService);
log(message: string) {
this._logger.log(message);
}
}14. Index.ts
Используем ТОЛЬКО для группировки DI зависимостей. НИКОГДА не для реэкспорта.
// ✅ Правильно
// services/index.ts
export const USER_SERVICES = [UserService, AuthService];
// ❌ Неправильно
// services/index.ts
export * from "./user.service";
export * from "./auth.service";Все импорты — по прямым путям:
// ✅ Правильно
import { UserService } from "../services/user.service";
// ❌ Неправильно
import { UserService } from "../services";Обработка ошибок
15. Try-Catch
Используем ТОЛЬКО для:
- HTTP запросов
- Запросов к БД
- Других асинхронных операций
// ✅ Правильно
async function fetchUser(id: string) {
try {
return await this.api.get<IUser>(`/users/${id}`);
} catch (error) {
console.error("Failed to fetch user", error);
return null;
}
}
// ❌ Неправильно
function processData(data: IData) {
try {
const result = data.value * 2;
return result;
} catch (error) {
// Это не нужно
}
}
// ✅ Правильно
function processData(data: IData | null) {
if (!data) return null;
return data.value * 2;
}16. Ошибки и логирование
// ❌ Неправильно
const ERROR_MESSAGES = {
USER_NOT_FOUND: "User not found"
};
this._logger.error(ERROR_MESSAGES.USER_NOT_FOUND, error);
// ✅ Правильно для логирования
this._logger.error("Failed to find user", error);
// ✅ Правильно для фронтенда (HTTP ошибки)
throw new NotFoundException("User not found");Именование
17. Файлы и сущности
Файлы называем с суффиксом типа:
user.interface.ts
user.service.ts
user.component.ts
user-list.component.ts
format-date.util.ts
create-user.request.ts
auth.guard.ts
auth.interceptor.ts18. Константы
Выносим в константы ТОЛЬКО когда TypeScript не помогает:
// ✅ Правильно
@Entity(USERS_TABLE_NAME)
@Get(USER_ENDPOINTS.PROFILE)
export class UserController {
@Get(ADMIN_ENDPOINTS.USERS)
getAdminUsers() {
// Декораторы — нет подсказок TypeScript
}
}
// ✅ Правильно
const users = await this.userRepository.find({
where: { email: userEmail }
// TypeScript знает поля, константы не нужны
});
// ❌ Неправильно
const users = await this.userRepository.find({
where: { [USER_FIELDS.EMAIL]: userEmail }
// Лишние константы для типизированных полей
});19. Префиксы констант
ВСЕГДА пишем с префиксом модуля:
// ✅ Правильно
export const INSTAGRAM_ENDPOINTS = {
AUTH: "/auth",
USER: "/user"
};
export const TELEGRAM_API_KEYS = {
BOT_TOKEN: "BOT_TOKEN",
WEBHOOK_URL: "WEBHOOK_URL"
};
// ❌ Неправильно
export const ENDPOINTS = {
/* ... */
};
export const API_KEYS = {
/* ... */
};Best Practices
✅ Делайте:
- Пишите code self-documenting
- Используйте типизацию на максимум
- Выносите бизнес-логику в сервисы
- Разбивайте на маленькие файлы
❌ Не делайте:
- Не пишите огромные компоненты (более 300 строк)
- Не используйте
anyтипы - Не пишите многоуровневую вложенность
- Не игнорируйте типы TypeScript
Чек-лист перед пушем
- [ ] Код типизирован (нет
any) - [ ] Используется early return pattern
- [ ] Нет явных return типов
- [ ] Сервисы компактные (< 100 строк)
- [ ] Использован DI через
inject() - [ ] Нет комментариев в коде
- [ ] Интерфейсы в отдельных файлах
- [ ] ESLint и Stylelint pass
- [ ] TypeScript typecheck pass
Следующие шаги
- Качество кода — ESLint, Stylelint, Prettier
- Workflow разработки — Git hooks
- Архитектура — структура проекта