Стандарты

TypeScript

Организация типов

app/types/
├── index.ts          # Re-exports
├── User.ts           # Пользователь
├── Order.ts          # Заказы
├── Service.ts        # Услуги
├── Company.ts        # Компании
├── Api.ts            # API типы (PaginatedResponse, etc.)
└── Common.ts         # Общие типы

Паттерны типизации

Entity типы

// app/types/User.ts
export interface User {
  id: string
  email: string
  firstName: string
  lastName: string
  phone: string | null
  avatar: string | null
  role: UserRole
  permissions: string[]
  createdAt: string
  updatedAt: string
}

export type UserRole = 'admin' | 'operator' | 'user'

// DTO для создания (без id и timestamps)
export type CreateUserDto = Omit<User, 'id' | 'createdAt' | 'updatedAt'>

// DTO для обновления (всё опционально)
export type UpdateUserDto = Partial<CreateUserDto>

Enum vs Union

// ✅ Union types (предпочтительно)
export type OrderStatus = 'pending' | 'processing' | 'completed' | 'cancelled'

// ✅ Enum (если нужны значения)
export enum OrderStatusEnum {
  Pending = 'pending',
  Processing = 'processing',
  Completed = 'completed',
  Cancelled = 'cancelled',
}

// С лейблами
export const ORDER_STATUS_LABELS: Record<OrderStatus, string> = {
  pending: 'Ожидает',
  processing: 'В работе',
  completed: 'Завершён',
  cancelled: 'Отменён',
}

API Response типы

// app/types/Api.ts
export interface PaginatedResponse<T> {
  data: T[]
  meta: PaginationMeta
}

export interface PaginationMeta {
  total: number
  page: number
  perPage: number
  lastPage: number
}

export interface ApiError {
  message: string
  code?: string
  details?: Record<string, string[]>
}

Компонент props

// В компоненте
interface Props {
  user: User
  readonly?: boolean
  onEdit?: (user: User) => void
}

const props = withDefaults(defineProps<Props>(), {
  readonly: false,
})

Правила

1. Используй interface для объектов

// ✅ interface для объектов
interface User {
  id: string
  name: string
}

// ✅ type для unions, primitives, utilities
type UserRole = 'admin' | 'user'
type UserId = string
type UserWithoutId = Omit<User, 'id'>

2. Избегай any

// ❌ Плохо
function process(data: any) { ... }

// ✅ Хорошо
function process<T>(data: T) { ... }

// ✅ Или используй unknown
function process(data: unknown) {
  if (typeof data === 'string') { ... }
}

3. Типизируй API ответы

// ❌ Плохо
const response = await api.get('/users')

// ✅ Хорошо
const response = await api.get<PaginatedResponse<User>>('/users')

4. Используй утилитарные типы

// Partial — все поля опциональны
type UpdateUserDto = Partial<User>

// Pick — только указанные поля
type UserCredentials = Pick<User, 'email' | 'password'>

// Omit — исключить поля
type CreateUserDto = Omit<User, 'id' | 'createdAt'>

// Required — все поля обязательны
type RequiredUser = Required<User>

// Record — словарь
type UserRoleLabels = Record<UserRole, string>

5. Типизируй template refs

import type { FormInstance } from 'ant-design-vue'

const formRef = ref<FormInstance>()
const inputRef = ref<HTMLInputElement>()

6. Не дублируй типы, которые выводятся автоматически

TypeScript умеет выводить типы из контекста. Не указывай типы там, где они очевидны.

// ❌ Избыточно — тип выводится из начального значения
const loading = ref<boolean>(false)
const count = ref<number>(0)
const name = ref<string>('')
const users = ref<User[]>([])

// ✅ Хорошо — TypeScript выведет тип сам
const loading = ref(false)           // Ref<boolean>
const count = ref(0)                 // Ref<number>
const name = ref('')                 // Ref<string>
const users = ref<User[]>([])        // Нужен — массив пустой, тип не выводится

// ❌ Избыточно — тип возврата очевиден
function getFullName(user: User): string {
  return `${user.firstName} ${user.lastName}`
}

// ✅ Хорошо — тип возврата выводится
function getFullName(user: User) {
  return `${user.firstName} ${user.lastName}`
}

// ❌ Избыточно — тип return composable
interface UseCounterReturn {
  count: Ref<number>
  increment: () => void
}
function useCounter(): UseCounterReturn {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

// ✅ Хорошо — тип выводится автоматически
function useCounter() {
  const count = ref(0)
  const increment = () => count.value++
  return { count, increment }
}

Когда типы НУЖНЫ:

  • Пустые массивы: ref<User[]>([])
  • Nullable значения: ref<User | null>(null)
  • API ответы: api.get<User>('/user')
  • Props компонентов: defineProps<Props>()
  • Сложные generic функции

Чек-лист

  • Типы в отдельных файлах (types/)
  • API ответы типизированы
  • Props компонентов типизированы
  • Нет any (используй unknown или generic)
  • Используются утилитарные типы

Связанные стандарты