← Назад к вопросам

Что реализовывал в типизации?

2.0 Middle🔥 141 комментариев
#Node.js и JavaScript#TypeScript

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Типизация в 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
  }
}

Результаты хорошей типизации

  1. IDE support — отличное автодополнение
  2. Ошибки на этапе разработки — не на production
  3. Self-documenting code — типы = документация
  4. Рефакторинг — TypeScript поймет где что менять
  5. Меньше тестов — типы заменяют 30% unit тестов
  6. Легче работать в команде — понятные контракты

Вывод: Типизация — это не просто инструмент, это инвестиция в качество и надежность кода. Я предпочитаю потратить 20% больше времени на типизацию, чем потом ловить баги в production.