$ sudo teach IT

Модуль 12.4

Частые ошибки и как их избегать

Антипаттерны TypeScript и как их исправить

Введение

Даже опытные разработчики допускают ошибки в TypeScript. Этот урок поможет вам узнать, распознать и избежать наиболее распространённых ловушек. Знание антипаттернов — это столь же важно, как и знание правильных подходов.

Мы рассмотрим каждую ошибку с двух сторон: почему это проблема и как исправить. Для каждой ошибки приведены примеры «до» и «после», а также рекомендации по альтернативам.

Ошибка 1: Злоупотребление типом any

Почему это проблема:

Тип any отключает проверку типов для значения. Этокак подмена лампочкибезопасный проверки на перегоревшую — вы не получите ошибку в компиляторе, но получите её в runtime.

// ❌ ПЛОХО: Использование any
function processData(data: any): any {
  return data.name.toUpperCase();  // Ошибка в runtime, если data не имеет name
}

// ❌ ПЛОХО: Неявный any (когда strict: false)
function calculate(x, y) {  // x и y — any
  return x + y;
}

// ❌ ПЛОХО: any для "быстрого решения"
const response: any = await fetch('/api');
const result: any = response.data;
console.log(result.nonExistentProperty.typo);  // Ошибка в runtime!

// ✅ ХОРОШО: Использование unknown
function processDataSafe(data: unknown): string {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    const user = data as { name: string };
    return user.name.toUpperCase();
  }
  throw new Error('Invalid data');
}

// ✅ ХОРОШО: Типизация с интерфейсом
interface UserData {
  name: string;
  age: number;
}

function processUser(data: UserData): string {
  return data.name.toUpperCase();
}

// ✅ ХОРОШО: Generic для переиспываемости
function processItem<T extends { name: string }>(item: T): string {
  return item.name.toUpperCase();
}
Правило:

Используйте unknown вместо any когда тип данных неизвестен. unknown безопаснее, так как требует проверки типа перед использованием.

Ошибка 2: Неправильное использование type vs interface

// ❌ ПЛОХО: Смешивание без понимания различий
type User = { name: string };
type Admin = User & { role: 'admin' };

// Когда нужно расширение — используйте interface:
// ❌ interface Admin extends User — правильнее для расширения

// ✅ ХОРОШО: Правильное применение

// Interface — для объектов и классов (поддерживает declaration merging)
interface UserInterface {
  name: string;
  email: string;
}

interface UserInterface {  // Declaration merging — расширение существующего типа
  age: number;
}

// Type — для union, intersection, tuple, примитивов
type ID = string | number;
type Coordinates = [number, number];
type Nullable<T> = T | null;

// Различия:
// 1. Declaration merging
interface Window { customProp: string; }  // ОК — расширение Window
// type Window = { customProp: string };  // Ошибка!

// 2. Union и intersection
type Status = 'active' | 'inactive';  // ОК
// interface Status { ... }  // ОК, но менее удобно

// 3. Tuple
type Pair = [string, number];  // ОК
// interface Pair extends Array<string | number> { 0: string; 1: number; }  // Сложно

// 4. Рекурсия (оба работают)
type RecursiveType = { value: string; children?: RecursiveType[] };
interface RecursiveInterface { value: string; children?: RecursiveInterface[] }

Когда что использовать:

Interface Объекты, классы, declaration merging, extends
Type Union, intersection, tuple, primitive aliases, mapped types
Правило По умолчанию interface для объектов; type для всего остального

Ошибка 3: Отсутствие strict mode

// ❌ ПЛОХО: tsconfig без strict mode
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext"
    // strict не включён!
  }
}

// Это означает:
// - strictNullChecks: false → null/undefined не проверяются
// - strictFunctionTypes: false → функции не строго типизируются
// - strictBindCallApply: false → bind/call/apply не проверяются
// - noImplicitAny: false → any добавляется автоматически
// - noImplicitThis: false → this может быть any

// ✅ ХОРОШО: Включение strict mode
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true
  }
}

// Последствия strictNullChecks: false
function getLength(s: string | null) {
  return s.length;  // Без strict — компилируется, ошибка в runtime
  // С strict — ошибка компиляции: Object is possibly 'null'
}

// С strict:
function getLengthSafe(s: string | null) {
  if (s === null) return 0;
  return s.length;  // Теперь безопасно
}

Ошибка 4: Неправильная работа с null и undefined

// ❌ ПЛОХО: Игнорирование nullable значений
function processUser(user: User | null) {
  // Ошибка: Object is possibly 'null'
  console.log(user.name);
}

// ❌ ПЛОХО: Злоупотребление non-null assertion (!)
function getUserName(user: User | null) {
  return user!.name;  // Обманывает компилятор
}

// ❌ ПЛОХО: Смешивание null и undefined
function find(id: number): User | null { return null; }
function findById(id: number): User | undefined { return undefined; }
// Разные возвращаемые типы — путаница в команде

// ✅ ХОРОШО: Проверка через optional chaining
function processUserSafe(user: User | null) {
  console.log(user?.name);  // undefined если user null
}

// ✅ ХОРОШО: Nullish coalescing
function getDisplayName(user: User | null) {
  return user?.name ?? 'Anonymous';
}

// ✅ ХОРОШО: Типизированный guard
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'name' in obj &&
    'email' in obj
  );
}

function process(data: unknown) {
  if (isUser(data)) {
    console.log(data.name);  // Безопасно
  }
}

// ✅ ХОРОШО: Ранний return
function processUserBest(user: User | null): string {
  if (!user) {
    return 'No user';
  }
  // Здесь user гарантированно User
  return user.name;
}

Ошибка 5: Неправильное использование enum

// ❌ ПЛОХО: Numeric enum (генерирует лишний JS код)
enum Direction {
  Up,      // 0
  Down,    // 1
  Left,    // 2
  Right,   // 3
}

// Компилируется в:
// var Direction;
// (function (Direction) {
//     Direction[Direction["Up"] = 0] = "Up";
//     Direction[Direction["Down"] = 1] = "Down";
//     Direction[Direction["Left"] = 2] = "Left";
//     Direction[Direction["Right"] = 3] = "Right";
// })(Direction || (Direction = {}));

// Проблема: Direction[0] === "Up", Direction["Up"] === 0
// Это может привести к неожиданному поведению

// ❌ ПЛОХО: Смешивание строковых enum с литералами
enum Status {
  Active = 'ACTIVE',
  Inactive = 'INACTIVE',
}

function isActive(status: Status): boolean {
  return status === 'ACTIVE';  // Ошибка: прямое сравнение со строкой
}

// ✅ ХОРОШО: As const объект (рекомендуемый подход)
const Direction = {
  Up: 'UP',
  Down: 'DOWN',
  Left: 'LEFT',
  Right: 'RIGHT',
} as const;

type Direction = typeof Direction[keyof typeof Direction];
// "UP" | "DOWN" | "LEFT" | "RIGHT"

// ✅ ХОРОШО: Union литералов
type Status = 'active' | 'inactive' | 'pending';

// ✅ ХОРОШО: String enum (если нужен enum)
enum BetterStatus {
  Active = 'ACTIVE',
  Inactive = 'INACTIVE',
  Pending = 'PENDING',
}

function isActive(status: BetterStatus): boolean {
  return status === BetterStatus.Active;  // Безопасно
}

// Сравнение подходов:
// 1. as const объект: лучшаяtree-shaking, нет лишнего JS кода
// 2. Union литералов: самый простой способ
// 3. String enum: когда нужен reverse mapping
// 4. Numeric enum: избегайте!

Ошибка 6: Забывание о strictFunctionTypes

// strictFunctionTypes: false (по умолчанию, если strict: false)
// Функции возвращаются в контравариантных позициях (аргументы)

// Пример без strictFunctionTypes:
interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

type ProcessAnimal = (animal: Animal) => void;
type ProcessDog = (dog: Dog) => void;

// Без strictFunctionTypes:
const processAnimal: ProcessAnimal = (animal) => {
  console.log(animal.name);
};

// Это "работает" — но небезопасно!
const processDog: ProcessDog = processAnimal;  // ❌ Опасно!
processDog({ name: 'Rex', breed: 'Labrador' });  // OK
processDog({ name: 'Rex' });  // Missing breed — ошибка в runtime!

// С strictFunctionTypes:
// const processDog: ProcessDog = processAnimal;  // Ошибка компиляции!

// ✅ ХОРОШО: Исправление через строгую типизацию
const processDogSafe: ProcessDog = (dog) => {
  console.log(dog.name, dog.breed);  // Безопасно
};

// Рекомендация: Всегда включайте strict: true в tsconfig

Ошибка 7: Неправильная типизация событий

// ❌ ПЛОХО: Использование Event вместо specific типа
function handleClick(event: Event) {
  // Ошибка: Property 'clientX' does not exist on type 'Event'
  console.log(event.clientX);
}

// ❌ ПЛОХО: Any для событий
function handleInput(event: any) {
  console.log(event.target.value);  // Без проверки типов
}

// ✅ ХОРОШО: Типизированные события React
import React from 'react';

function handleClick(event: React.MouseEvent<HTMLButtonElement>) {
  console.log(event.clientX, event.clientY);  // OK
}

function handleInput(event: React.ChangeEvent<HTMLInputElement>) {
  console.log(event.target.value);  // OK
}

function handleFormSubmit(event: React.FormEvent<HTMLFormElement>) {
  event.preventDefault();
  const formData = new FormData(event.currentTarget);
  // ...
}

// ✅ ХОРОШО: Типизация DOM событий
function handleScroll(event: Event) {
  const target = event.target as Window;
  console.log(target.scrollY);
}

// Или лучше:
function handleScroll(event: UIEvent) {
  const target = event.target as Element;
  console.log(target.scrollTop);
}

// ✅ ХОРОШО: Кастомные типизированные обработчики
type ClickHandler = (event: React.MouseEvent<HTMLButtonElement>) => void;
type ChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => void;

interface ButtonProps {
  onClick: ClickHandler;
  label: string;
}

function Button({ onClick, label }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>;
}

Ошибка 8: Избыточное использование assertion (as)

// ❌ ПЛОХО: Assertion для обхода проверок
function process(data: unknown) {
  const user = data as User;  // Обман компилятора
  console.log(user.name);  // Может упасть в runtime
}

// ❌ ПЛОХО: Множественные assertion
const response = await fetch('/api');
const data = response.json() as User;  // Double assertion
const name = (data as any).name;  // Triple assertion

// ❌ ПЛОХО: Assertion для приведения типов
function getLength(value: string | null): number {
  return (value as string).length;  // Опасно если value === null
}

// ✅ ХОРОШО: Type guard вместо assertion
function isUser(data: unknown): data is User {
  return (
    typeof data === 'object' &&
    data !== null &&
    'name' in data &&
    'email' in data
  );
}

function processSafe(data: unknown) {
  if (isUser(data)) {
    console.log(data.name);  // Безопасно
  }
}

// ✅ ХОРОШО: Assertion functions
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

function process(input: unknown) {
  assertIsString(input);
  console.log(input.toUpperCase());  // Безопасно
}

// ✅ ХОРОШО: Assertion с проверкой
function getUserSafe(data: unknown): User | null {
  if (isUser(data)) {
    return data;
  }
  return null;
}

Ошибка 9: Неправильная работа с дженериками

// ❌ ПЛОХО: Слишком широкие constraints
function getFirst<T>(items: T[]): T {
  return items[0];  // T может быть undefined
}

const result = getFirst([]);  // result — T, но массив пустой!

// ❌ ПЛОХО: Отсутствие default параметра
function fetchData<T>(): Promise<T> {
  // T неизвестен, difficult to use
}

// ❌ ПЛОХО: Использование any в generics
function merge<T>(a: T, b: any): T {
  return { ...a, ...b };  // b может содержать что угодно
}

// ✅ ХОРОШО: Ограниченный generic с default
function getFirstSafe<T>(items: T[]): T | undefined {
  return items[0];
}

const first = getFirstSafe([1, 2, 3]);  // number | undefined

// ✅ ХОРОШО: Generic с constraints
function mergeObjects<T extends object, U extends object>(a: T, b: U): T & U {
  return { ...a, ...b };
}

const merged = mergeObjects({ name: 'Alice' }, { age: 30 });
// { name: string; age: number }

// ✅ ХОРОШО: Generic сусловиеными типами
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Alice', age: 30 };
const name = getProperty(user, 'name');  // string
const age = getProperty(user, 'age');    // number

// ✅ ХОРОШО: Generic с default и constraints
interface ApiResponse<T = unknown> {
  data: T;
  status: number;
  message: string;
}

type UserResponse = ApiResponse<User>;  // T = User
type GenericResponse = ApiResponse<>;    // T = unknown

Ошибка 10: Циклические зависимости типов

// ❌ ПЛОХО: Взаимные зависимости
// user.ts
import { Order } from './order';
interface User {
  id: string;
  orders: Order[];
}

// order.ts
import { User } from './user';
interface Order {
  id: string;
  user: User;  // Циклическая зависимость!
}

// ✅ ХОРОШО: Вынос общих типов в отдельный файл
// types/user.ts
export interface User {
  id: string;
  orderIds: string[];  // Только ID, не полные объекты
}

// types/order.ts
export interface Order {
  id: string;
  userId: string;  // Только ID
}

// Или использовать interface с partial типом:
// types/common.ts
export interface User {
  id: string;
  name: string;
}

export interface Order {
  id: string;
  userId: string;
}

// types/userWithOrders.ts
import { User, Order } from './common';
export interface UserWithOrders extends User {
  orders: Order[];
}

// types/orderWithUser.ts
import { User, Order } from './common';
export interface OrderWithUser extends Order {
  user: User;
}

Ошибка 11: Неправильная типизация API ответов

// ❌ ПЛОХО: Any для API ответа
async function fetchUser() {
  const response = await fetch('/api/user');
  const data: any = await response.json();
  console.log(data.name);  // Без проверки
}

// ❌ ПЛОХО: Нет обработки ошибок
async function fetchData() {
  const response = await fetch('/api/data');
  return response.json();  // Может быть ошибкой
}

// ✅ ХОРОШО: Типизированный API клиент
interface ApiResponse<T> {
  data: T;
  status: number;
  message?: string;
}

interface ApiError {
  status: number;
  message: string;
  code?: string;
}

async function fetchApi<T>(url: string): Promise<T> {
  const response = await fetch(url);

  if (!response.ok) {
    const error: ApiError = await response.json();
    throw new Error(error.message || 'API Error');
  }

  const result: ApiResponse<T> = await response.json();
  return result.data;
}

// Использование
interface User {
  id: string;
  name: string;
  email: string;
}

async function getUser(id: string): Promise<User> {
  return fetchApi<User>(`/api/users/${id}`);
}

// ✅ ХОРОШО: Valibot/Zod для runtime валидации
import { z } from 'zod';

const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

async function getUserSafe(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return UserSchema.parse(data);  // Валидация в runtime
}

Ошибка 12: Игнорирование readonly

// ❌ ПЛОХО: Мутация аргументов
function sortUsers(users: User[]): User[] {
  return users.sort((a, b) => a.name.localeCompare(b.name));
  // Оригинальный массив мутирует!
}

// ✅ ХОРОШО: Копирование перед мутацией
function sortUsersSafe(users: readonly User[]): User[] {
  return [...users].sort((a, b) => a.name.localeCompare(b.name));
}

// ✅ ХОРОШО: ReadonlyArray в параметрах
function printUsers(users: readonly User[]) {
  users.forEach(user => console.log(user.name));
  // users.push(...) — ошибка компиляции!
}

// ✅ ХОРОШО: Readonly объекты
interface Config {
  readonly host: string;
  readonly port: number;
}

const config: Config = { host: 'localhost', port: 3000 };
config.port = 8080;  // Ошибка: Cannot assign to 'port'

// ✅ ХОРОШО: Frozen объекты
const frozenConfig = Object.freeze({
  host: 'localhost',
  port: 3000,
});
frozenConfig.port = 8080;  // Ошибка

// ✅ ХОРОШО: As const для литеральных объектов
const routes = {
  home: '/',
  about: '/about',
  contact: '/contact',
} as const;

routes.home = '/new';  // Ошибка: readonly

Ошибка 13: Неправильное использование union типов

// ❌ ПЛОХО: Широкие union без дискриминации
type Result = SuccessResult | ErrorResult;

interface SuccessResult {
  type: 'success';
  data: any;
}

interface ErrorResult {
  type: 'error';
  message: string;
}

// Обработка без дискриминации
function handleResult(result: Result) {
  if (result.type === 'success') {
    console.log(result.data);  // OK
  } else {
    console.log(result.message);  // OK
  }
}

// ❌ ПЛОХО: Использование in для проверки
function process(value: string | number) {
  if ('toUpperCase' in value) {
    // value — string, но это не надёжно для примитивов
  }
}

// ✅ ХОРОШО: Дискриминированные union
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default:
      const _exhaustive: never = shape;  // Проверка исчерпывающего покрытия
      return _exhaustive;
  }
}

// ✅ ХОРОШО: Type guard для union
function isSuccessResult(result: Result): result is SuccessResult {
  return result.type === 'success';
}

function handleResultSafe(result: Result) {
  if (isSuccessResult(result)) {
    console.log(result.data);  // Безопасно
  } else {
    console.log(result.message);  // Безопасно
  }
}

Ошибка 14: Отсутствие типизации ошибок

// ❌ ПЛОХО: Ловля ошибок без типизации
async function fetchData() {
  try {
    const response = await fetch('/api');
    return response.json();
  } catch (error) {
    console.log(error);  // error — any
    return null;
  }
}

// ❌ ПЛОХО: Unknown без проверки
async function fetchDataSafe() {
  try {
    const response = await fetch('/api');
    return response.json();
  } catch (error) {
    // error — unknown
    console.log(error.message);  // Ошибка компиляции
    return null;
  }
}

// ✅ ХОРОШО: Типизированная обработка ошибок
interface ApiError {
  message: string;
  code: string;
  statusCode: number;
}

function isApiError(error: unknown): error is ApiError {
  return (
    typeof error === 'object' &&
    error !== null &&
    'message' in error &&
    'code' in error
  );
}

async function fetchDataTyped() {
  try {
    const response = await fetch('/api');
    if (!response.ok) {
      const error: ApiError = await response.json();
      throw error;
    }
    return response.json();
  } catch (error) {
    if (isApiError(error)) {
      console.error(`${error.code}: ${error.message}`);
    } else if (error instanceof Error) {
      console.error(error.message);
    } else {
      console.error('Unknown error');
    }
    return null;
  }
}

// ✅ ХОРОШО: Кастомные ошибки
class ValidationError extends Error {
  constructor(
    message: string,
    public fields: Record<string, string[]>,
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

class NotFoundError extends Error {
  constructor(resource: string, id: string) {
    super(`${resource} with id ${id} not found`);
    this.name = 'NotFoundError';
  }
}

function processUser(id: string) {
  try {
    const user = getUser(id);
    if (!user) {
      throw new NotFoundError('User', id);
    }
    return user;
  } catch (error) {
    if (error instanceof NotFoundError) {
      console.error(error.message);
    } else if (error instanceof ValidationError) {
      console.error(error.fields);
    }
    throw error;
  }
}

Ошибка 15: Неправильная работа с Promise типами

// ❌ ПЛОХО: Забывание await
async function processData() {
  const response = fetch('/api');  // Промис, не результат
  console.log(response);  // Promise { ... }, не данные
}

// ❌ ПЛОХО: Обработка промиса без await
function getData() {
  fetch('/api')
    .then(res => res.json())
    .then(data => {
      return data;  // Возврат не работает с .then!
    });
}

// ❌ ПЛОХО: Неправильная типизация async функции
function getUser(): User {  // Ошибка: возвращает Promise
  return async () => {
    const response = await fetch('/api/user');
    return response.json();
  };
}

// ✅ ХОРОШО: Правильный async/await
async function getDataSafe() {
  const response = await fetch('/api');
  const data = await response.json();
  return data;
}

// ✅ ХОРОШО: Типизация async функции
async function getUserTyped(): Promise<User> {
  const response = await fetch('/api/user');
  return response.json();
}

// ✅ ХОРОШО: Обработка ошибок в async
async function fetchWithRetry<T>(url: string, retries = 3): Promise<T> {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      return response.json();
    } catch (error) {
      if (i === retries - 1) {
        throw error;
      }
      await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
    }
  }
  throw new Error('Max retries reached');
}

Ошибка 16: Злоупотребление функциональной типизацией

// ❌ ПЛОХО: Избыточная функциональная типизация
type Mapper<T, U> = (item: T) => U;
type Filter<T> = (item: T) => boolean;
type Reducer<T, U> = (acc: U, item: T) => U;
type Predicate<T> = (item: T) => boolean;
type Consumer<T> = (item: T) => void;
type Supplier<T> = () => T;

// Слишком много типов для простых операций

// ✅ ХОРОШО: Использование встроенных типов
// Array methods уже типизированы
const numbers = [1, 2, 3, 4, 5];

// Маппинг
const doubled = numbers.map((n: number): number => n * 2);

// Фильтрация
const evens = numbers.filter((n: number): boolean => n % 2 === 0);

// Редукция
const sum = numbers.reduce((acc: number, n: number): number => acc + n, 0);

// ✅ ХОРОШО: Простые type aliases
type UserId = string;
type Email = string;
type Timestamp = number;

// Не создавайте type для каждого простого типа

Чек-лист перед код ревью

Проверьте свой код перед ревью:

☐ Нет использования any — используйте unknown или конкретные типы
☐ Включён strict: true в tsconfig.json
☐ Nullable значения проверяются через optional chaining или guard
☐ Non-null assertion (!) не используется без обоснованной причины
☐ Enum заменены на as const объекты или union литералы
☐ API ответы типизированы и валидируются
☐ Ошибки типизируются и обрабатываются
☐ Async/await используется корректно
☐ Readonly используется для иммутабельных данных
☐ Union типы имеют дискриминант для безопасного сужения
☐ Generics имеют правильные constraints
☐ Type assertions используются только когда необходимо
☐ Код проходит typecheck без ошибок

Итоги урока

Мы рассмотрели 16 частых ошибок TypeScript и способы их решения:

  • any → используйте unknown или конкретные типы
  • type vs interface — interface для объектов/классов, type для union/tuple
  • strict mode — всегда включайте в новых проектах
  • null/undefined — проверяйте через optional chaining и guards
  • enum — предпочитайте as const объекты
  • assertion — используйте type guards вместо as
  • generics — добавляйте constraints и default параметры
  • API типы — валидируйте в runtime с Zod/Valibot
  • ошибки — типизируйте и обрабатывайте
  • async — не забывайте await

Запомните: TypeScript — ваш друг, не враг. Если компилятор ругается, значит он пытается вас защитить. Не обходите проверки — исправляйте код.

Частые ошибки и как их избегать

10 вопросов

Исправление типичных ошибок TypeScript

Premium