Что реализовывал в типизации?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Типизация в TypeScript: реальные примеры из моей практики
1. Строгая типизация API контрактов
В моих проектах я использую типизацию для документирования и валидации API. Вот как я работаю с платежной системой:
// types/payment.ts
type PaymentStatus = 'pending' | 'completed' | 'failed' | 'refunded';
type PaymentMethod = 'credit_card' | 'paypal' | 'crypto';
interface PaymentRequest {
userId: string;
amount: number; // в центах
currency: 'USD' | 'EUR' | 'RUB';
method: PaymentMethod;
metadata?: Record<string, unknown>;
}
interface PaymentResponse {
id: string;
status: PaymentStatus;
amount: number;
fee: number;
net: number;
createdAt: Date;
completedAt?: Date;
}
interface PaymentError {
code: 'INSUFFICIENT_FUNDS' | 'INVALID_CARD' | 'RATE_LIMIT' | 'NETWORK_ERROR';
message: string;
retryable: boolean;
}
Теперь все, кто использует платежный API, видят точный контракт:
class PaymentService {
async processPayment(request: PaymentRequest): Promise<PaymentResponse | PaymentError> {
// TypeScript не позволит передать неправильный amount тип
// Не позволит забыть userId
// Не позволит использовать неправильный PaymentMethod
}
}
2. Типизация БД моделей и схем
Для MongoDB я создавал типизированные модели с автодополнением:
// types/database.ts
interface IUser {
_id: ObjectId;
email: string;
name: string;
role: 'admin' | 'moderator' | 'user';
subscription: {
tier: 'free' | 'pro' | 'enterprise';
expiresAt: Date;
autoRenewal: boolean;
};
createdAt: Date;
updatedAt: Date;
}
interface IPost {
_id: ObjectId;
userId: ObjectId; // ссылка на User
title: string;
content: string;
tags: string[];
likes: number;
comments: Array<{
userId: ObjectId;
text: string;
createdAt: Date;
}>;
published: boolean;
createdAt: Date;
}
А затем в сервисе:
class UserService {
async getUserById(id: string): Promise<IUser> {
// TypeScript знает все поля IUser
// Автодополнение работает идеально
const user = await User.findById(id);
// TypeScript: user.email — string
// TypeScript: user.subscription.tier — 'free' | 'pro' | 'enterprise'
return user;
}
async updateSubscription(
userId: string,
tier: IUser['subscription']['tier']
): Promise<void> {
// tier уже типизирован как 'free' | 'pro' | 'enterprise'
await User.updateOne(
{ _id: userId },
{ 'subscription.tier': tier }
);
}
}
3. Conditional Types для сложной логики
Для фильтрации и поиска я создавал типы, которые меняются в зависимости от параметров:
// Если searchType = 'email', можем искать только по email
// Если searchType = 'phone', можем искать только по phone
type SearchFieldType<T extends 'email' | 'phone'> = T extends 'email'
? { field: 'email'; value: string }
: T extends 'phone'
? { field: 'phone'; value: string }
: never;
function searchUser<T extends 'email' | 'phone'>(
searchType: T,
query: SearchFieldType<T>
): Promise<IUser | null> {
if (searchType === 'email') {
// TypeScript: query.field === 'email'
// TypeScript: query.value — string
return User.findOne({ email: query.value });
} else {
// TypeScript: query.field === 'phone'
return User.findOne({ phone: query.value });
}
}
// Использование:
searchUser('email', { field: 'email', value: 'test@example.com' }); // OK
searchUser('email', { field: 'phone', value: '+1234' }); // ОШИБКА в TypeScript
searchUser('phone', { field: 'phone', value: '+1234' }); // OK
4. Generics для переиспользуемых сервисов
Для CRUD операций я создал базовый сервис с типизацией:
interface IRepository<T extends { _id: ObjectId }> {
findById(id: string): Promise<T | null>;
findAll(filter?: Partial<T>): Promise<T[]>;
create(data: Omit<T, '_id' | 'createdAt' | 'updatedAt'>): Promise<T>;
update(id: string, data: Partial<T>): Promise<T>;
delete(id: string): Promise<boolean>;
}
class BaseRepository<T extends { _id: ObjectId }> implements IRepository<T> {
constructor(private model: Model<T>) {}
async findById(id: string): Promise<T | null> {
return this.model.findById(id);
}
async create(data: Omit<T, '_id' | 'createdAt' | 'updatedAt'>): Promise<T> {
const doc = new this.model(data);
return doc.save();
}
}
// Использование:
class UserRepository extends BaseRepository<IUser> {
constructor(model: Model<IUser>) {
super(model);
}
// Все методы уже типизированы для IUser
}
5. Type Guards для runtime валидации
Для безопасности добавлял runtime проверки типов:
// Type Guard
function isPaymentMethod(value: unknown): value is PaymentMethod {
return (
typeof value === 'string' &&
['credit_card', 'paypal', 'crypto'].includes(value)
);
}
function isPaymentRequest(value: unknown): value is PaymentRequest {
return (
typeof value === 'object' &&
value !== null &&
'userId' in value &&
'amount' in value &&
'currency' in value &&
'method' in value &&
typeof (value as any).userId === 'string' &&
typeof (value as any).amount === 'number' &&
isPaymentMethod((value as any).method)
);
}
// Использование в контроллере
@Post('payments')
process(@Body() body: unknown) {
if (!isPaymentRequest(body)) {
throw new BadRequestException('Invalid payment request');
}
// Теперь body типизирован как PaymentRequest
return this.paymentService.process(body);
}
6. Utility Types для превращения типов
Для форм я использовал Utility Types:
// CreateUserDto = все поля IUser кроме _id, createdAt, updatedAt
type CreateUserDto = Omit<IUser, '_id' | 'createdAt' | 'updatedAt'>;
// UpdateUserDto = все поля опциональны
type UpdateUserDto = Partial<CreateUserDto>;
// UserPublic = только публичные поля
type UserPublic = Omit<IUser, 'email' | 'phone' | 'subscription'>;
// Readonly версия
type ReadonlyUser = Readonly<IUser>;
Использование:
@Post('users')
createUser(@Body() dto: CreateUserDto): Promise<IUser> {
// dto.email — обязателен
// dto._id — недоступен (удален через Omit)
return this.userService.create(dto);
}
@Patch('users/:id')
updateUser(
@Param('id') id: string,
@Body() dto: UpdateUserDto
): Promise<IUser> {
// Все поля опциональны
return this.userService.update(id, dto);
}
@Get('users/:id/public')
getPublicUser(@Param('id') id: string): Promise<UserPublic> {
// Возвращаем только публичные поля
const user = await this.userService.getById(id);
return user as UserPublic;
}
7. Типизация асинхронных операций
Для работы с Promise я создавал правильные типы:
// Извлечение типа из Promise
type Awaited<T> = T extends Promise<infer U> ? U : T;
type UserPromise = Promise<IUser>;
type ResolvedUser = Awaited<UserPromise>; // IUser
// API операции
type ApiResponse<T> = {
success: boolean;
data?: T;
error?: string;
};
async function callApi<T>(
endpoint: string
): Promise<ApiResponse<T>> {
try {
const response = await fetch(endpoint);
const data: T = await response.json();
return { success: true, data };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
// Использование
const response = await callApi<IUser>('/api/users/1');
if (response.success && response.data) {
console.log(response.data.email); // TypeScript: string
}
8. Типизация конфигурации
Для environment переменных:
interface AppConfig {
port: number;
mongoUri: string;
jwtSecret: string;
paymentGateway: {
apiKey: string;
baseUrl: string;
timeout: number;
};
logging: {
level: 'debug' | 'info' | 'warn' | 'error';
format: 'json' | 'text';
};
}
function loadConfig(): AppConfig {
const config: AppConfig = {
port: parseInt(process.env.PORT || '3000'),
mongoUri: process.env.MONGO_URI || 'mongodb://localhost',
jwtSecret: process.env.JWT_SECRET || 'dev-secret',
paymentGateway: {
apiKey: process.env.PAYMENT_API_KEY || '',
baseUrl: process.env.PAYMENT_BASE_URL || '',
timeout: 30000,
},
logging: {
level: 'info',
format: 'json',
},
};
if (!config.paymentGateway.apiKey) {
throw new Error('PAYMENT_API_KEY not set');
}
return config;
}
9. Типизация Event Emitters
Для асинхронных событий:
interface UserEvents {
'user:created': (user: IUser) => void;
'user:updated': (userId: string, changes: Partial<IUser>) => void;
'user:deleted': (userId: string) => void;
}
class UserEventEmitter extends EventEmitter {
emit<K extends keyof UserEvents>(
event: K,
...args: Parameters<UserEvents[K]>
): boolean {
return super.emit(event, ...args);
}
on<K extends keyof UserEvents>(
event: K,
listener: UserEvents[K]
): this {
return super.on(event, listener);
}
}
// Использование
const emitter = new UserEventEmitter();
emitter.emit('user:created', user); // TypeScript проверяет тип user
emitter.on('user:created', (user) => {
// TypeScript: user — IUser
console.log(user.email);
});
10. Строгая типизация в tsconfig.json
Для всего проекта я использую:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
Результаты хорошей типизации
- IDE support — отличное автодополнение
- Ошибки на этапе разработки — не на production
- Self-documenting code — типы = документация
- Рефакторинг — TypeScript поймет где что менять
- Меньше тестов — типы заменяют 30% unit тестов
- Легче работать в команде — понятные контракты
Вывод: Типизация — это не просто инструмент, это инвестиция в качество и надежность кода. Я предпочитаю потратить 20% больше времени на типизацию, чем потом ловить баги в production.