Skip to content

Правила написания кода

Проект следует строгим правилам написания кода для обеспечения качества, читаемости и консистентности.

Основные принципы

1. Return типы функций

НИКОГДА не пишем explicit return type для функций. TypeScript сам выведет тип.

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 вместо вложенных условий.

typescript
// ❌ Неправильно
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, выносим в отдельный файл.

typescript
// ✅ Структура
// 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 параметров, используем объект с интерфейсом.

typescript
// ❌ Неправильно
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. Интерфейсы

Интерфейсы ВСЕГДА выносим в отдельные файлы.

typescript
// ✅ Правильная структура
// 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. Именование переменных

Используем краткие понятные версии.

typescript
// ❌ Неправильно
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. Модульная структура

Используем структуру с разделением по сущностям.

typescript
// ✅ Структура
companies/
├── interfaces/
│   └── company.interface.ts
├── services/
│   └── companies.service.ts
├── components/
│   └── company-list.component.ts
├── pages/
│   └── companies-page.component.ts
└── companies.routes.ts

8. Signals & toSignal для компонентов

ВСЕГДА используем toSignal() для преобразования Observables в Signals:

typescript
// ❌ Старый стиль (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. Комментарии

НЕ пишем комментарии к коду. Код должен быть самодокументируемым.

typescript
// ❌ Неправильно
// 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. Декомпозиция

НЕ выносим функцию, если она вызывается один раз. Исключение — утилиты.

typescript
// ❌ Неправильно
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 — полоса вверху страницы:

typescript
// Автоматический loading для POST/PUT/DELETE через LoadingService
this.service.save(data).pipe(this.loadingService.withLoading()).subscribe();

Toast уведомления

Используем ToastrService для показа сообщений:

typescript
// Через 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 строк).

typescript
// ✅ Правильный размер сервиса
@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).

typescript
// ✅ Правильно (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 и префиксом _:

typescript
// ✅ Правильно
@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 зависимостей. НИКОГДА не для реэкспорта.

typescript
// ✅ Правильно
// services/index.ts
export const USER_SERVICES = [UserService, AuthService];

// ❌ Неправильно
// services/index.ts
export * from "./user.service";
export * from "./auth.service";

Все импорты — по прямым путям:

typescript
// ✅ Правильно
import { UserService } from "../services/user.service";

// ❌ Неправильно
import { UserService } from "../services";

Обработка ошибок

15. Try-Catch

Используем ТОЛЬКО для:

  • HTTP запросов
  • Запросов к БД
  • Других асинхронных операций
typescript
// ✅ Правильно
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. Ошибки и логирование

typescript
// ❌ Неправильно
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.ts

18. Константы

Выносим в константы ТОЛЬКО когда TypeScript не помогает:

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. Префиксы констант

ВСЕГДА пишем с префиксом модуля:

typescript
// ✅ Правильно
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

Следующие шаги

SaaS Admin Documentation