Модуль 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.jsonas const объекты или union литералыИтоги урока
Мы рассмотрели 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 вопросов