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'— берёт.defaultimport * 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?: number ≠ x: number | undefined. Полезно для строгих API.
Async и Promise
async функция всегда возвращает Promise
Если забыл await — получаешь Promise<T> вместо T.
Promise.all теряет тип при ошибке
Promise.all([fetchA(), fetchB()]) — если один отвалится, второй продолжает крутиться (нет abort).
void ≠ undefined в 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.
Связанные карты
- _MOC TypeScript — общая навигация по TS
- const enum vs enum — фокус на enum-pitfalls
- TS Narrowing и Type Guards — паттерны narrowing
- interface vs type -- когда что — выбор между interface и type alias
- any, unknown -- различия — отказ от any
- Discriminated Unions — паттерн дискриминированного union
- Generics — generics и constraints
Ресурсы
- TypeScript Handbook
- tsconfig reference
- Total TypeScript — продвинутые паттерны