TS — подводные камни

Каталог типичных ошибок TypeScript: где компилятор молчит, а должен ругаться, и где он ругается по делу, но непонятно.

Типы и инференс

any молча отключает проверку

  • any пробрасывается через цепочки и съедает все ошибки.
  • Решение: unknown + narrowing, никогда any.
  • См. any, unknown -- различия

Type assertion as — обещание, не проверка

const u = JSON.parse(raw) as User;  // 💥 ложь

Парсер вернёт что угодно. Реальная проверка — type guard или zod/io-ts.

Non-null assertion ! — то же самое

const el = document.getElementById('x')!;  // 💥 если null — runtime crash

Решение: явный if-guard или Optional chaining.

Type alias vs interface — не всегда взаимозаменяемы

  • Interface — расширяется через extends, открытое объявление (declaration merging)
  • Type alias — & для пересечения, union, mapped/conditional
  • См. interface vs type -- когда что

Числа и enum

Числовой enum принимает любое число

enum Status { Active, Inactive }  // 0, 1
function set(s: Status) {}
set(999);  // ✅ компилируется, runtime bug

Решение: строковый enum или union.

const enum + isolatedModules: true = ошибка

Vite, Next.js, esbuild, swc — не работают с const enum. См. const enum vs enum.

Утилитарные типы — частые ловушки

Partial<T> глубоко не работает

Partial<T> делает только верхний уровень опциональным. Для глубокой — нужен рекурсивный тип DeepPartial.

Readonly<T> поверхностный

То же самое — только верхний уровень. Внутренние объекты можно мутировать.

Pick/Omit забывают про дискриминированные union'ы

type A = { kind: 'a'; x: number };
type B = { kind: 'b'; y: string };
type AB = A | B;

type Out = Omit<AB, 'kind'>;  // НЕ ('a' | 'b')-aware

Решение: distributive conditional types.

Generics

Generic параметр без constraint = unknown

function f<T>(x: T) { x.length }  // ❌ ошибка
function g<T extends { length: number }>(x: T) { x.length }  // ✅

Слишком много generic параметров = непонятно

Каждый generic — это «дырка» в API. Чем меньше, тем читабельнее.

T | undefined vs optional parameter

function a(x?: number) {}            // a() — OK
function b(x: number | undefined) {} // b() — ошибка, b(undefined) — OK

Narrowing и type guards

typeof null === "object" — JS-классика

if (typeof x === 'object') {
  // x: object | null  ⚠️ null может быть!
}

Array.isArray(x) сужает к any[], а не T[]

Если работаешь с unknown, после Array.isArray тип становится any[]. Используй type predicate.

Discriminated union без discriminant не работает

type Shape = { x: number } | { y: number };
function area(s: Shape) {
  if ('x' in s) { /* s: { x: number } */ }
}

Лучше — явный дискриминант: type Shape = { kind: 'a'; x } | { kind: 'b'; y }.

Импорты / экспорты

import type vs import — для tree-shaking

import type { User } from './types';  // удаляется при компиляции
import { User } from './types';        // может оставить runtime-side-effects

Default import + namespace import = разные вещи

  • import x from 'lib' — берёт .default
  • import * as x from 'lib' — берёт весь namespace
  • Может ломать esModuleInterop=false проекты.

Circular dependencies — runtime undefined

TS компилирует, JS падает в undefined при импорте до инициализации.

Конфигурация (tsconfig.json)

strict: false = type-чек практически выключен

Включает 7 опций: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, alwaysStrict. Без них TS бесполезен.

noUncheckedIndexedAccess: true — ВКЛЮЧИТЬ

const arr = [1, 2, 3];
const x = arr[10];  // без опции: number 💥
                    // с опцией: number | undefined ✅

exactOptionalPropertyTypes: true — тонко

Делает x?: numberx: number | undefined. Полезно для строгих API.

Async и Promise

async функция всегда возвращает Promise

Если забыл await — получаешь Promise<T> вместо T.

Promise.all теряет тип при ошибке

Promise.all([fetchA(), fetchB()]) — если один отвалится, второй продолжает крутиться (нет abort).

voidundefined в callbacks

type Cb = () => void;
const cb: Cb = () => 42;  // ✅ компилируется
// 42 нельзя использовать как T, но возврат разрешён

Часто ломает понимание: void в return type = «не используй возвращаемое значение».

Классы

strictPropertyInitialization ловит uninitialized

class User {
  name: string;     // ❌ ошибка — не инициализировано
  age: number = 0;  // ✅
  surname!: string; // ✅ но это обещание разработчика
}

private vs #private

  • private — TS-only, в JS остаётся доступным (через obj['x'])
  • #private — true ECMAScript private (с TS 4.3+), реальная инкапсуляция в рантайме

Method binding теряется при передаче

class A { x = 1; m() { return this.x } }
const a = new A();
const m = a.m;  // m() → undefined, потеря this

Решение: arrow в class field или .bind(this).

Производительность

as const создаёт глубокие readonly-типы

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

Conditional/Mapped/Recursive типы — медленные

Тяжёлые типы (Object-Path, Recursive Pick) могут увеличить время сборки на 10x.

Lookup типы (T['key']) дешевле чем Pick<T, 'key'>

Если нужен только один ключ — лучше lookup.

Связанные карты

Ресурсы