27. Паттерны оптимизации
TypeScript: Паттерны оптимизации
Заголовок раздела «TypeScript: Паттерны оптимизации»Привет, мой дорогой друг, разработчик с большой буквы! Яша снова здесь, и сегодня мы нырнем в мир, где код не просто работает, а работает быстро и эффективно. Мы поговорим о паттернах оптимизации в TypeScript. Но не спеши думать, что мы будем заниматься микрооптимизациями или ковыряться в байт-коде. Нет! В TypeScript оптимизация — это не только про runtime производительность, но и про ускорение работы компилятора, улучшение DX (Developer Experience) и, конечно же, про написание более поддерживаемого и предсказуемого кода.
Представь, что твой код — это роскошный спортивный автомобиль. TypeScript помогает тебе не только собрать его по чертежам, но и убедиться, что все детали на своих местах, а двигатель работает без перебоев. Паттерны оптимизации — это тюнинг, который делает этот автомобиль не просто красивым, но и невероятно мощным.
🚗 Оптимизация на уровне типов (Compile-Time)
Заголовок раздела «🚗 Оптимизация на уровне типов (Compile-Time)»Да-да, TypeScript умеет оптимизировать даже на этапе компиляции! Это не про выполнение кода, а про скорость работы самого компилятора и про то, как он обрабатывает сложные типы. Чрезмерно сложные или рекурсивные типы могут замедлять компиляцию, и тут нам на помощь приходят техники “мемоизации” типов.
Мемоизация типов с помощью условных типов
Заголовок раздела «Мемоизация типов с помощью условных типов»Идея проста: если мы уже вычислили сложный тип, зачем вычислять его снова? TypeScript не предоставляет явного механизма кэширования результатов вычисления типов, но мы можем использовать условные типы (A extends B ? X : Y) и infer для имитации этого поведения, переиспользуя результаты внутри более сложных структур.
Представь, что у нас есть сложный тип, который “глубоко читает” структуру объекта.
// Определим некий сложный тип, который что-то делает с вложенными свойствамиtype DeepExtractProperty<T, K extends keyof T> = T[K] extends object ? { [P in keyof T[K]]: T[K][P] } : T[K];
// А теперь представим, что мы хотим "кэшировать" результат для конкретного T и K// Это не настоящая мемоизация компилятора, но хороший паттерн для переиспользования// промежуточных результатов в сложных типах.type MemoizedResult<T, K extends keyof T, Result = DeepExtractProperty<T, K>> = Result;
interface UserProfile { id: number; details: { name: string; email: string; address: { city: string; zip: number; }; }; isActive: boolean;}
// Теперь мы используем MemoizedResult, чтобы указать, что мы ожидаем определенный тип// Это не ускоряет компиляцию напрямую, но помогает структурировать сложные типы// и предотвратить повторные "дорогие" вычисления в рамках одной цепочки типов.type UserDetailsType = MemoizedResult<UserProfile, 'details'>;
const userDetails: UserDetailsType = { name: "Яша", address: { city: "TypeScriptville", zip: 12345 }};
console.log(userDetails);
// Пример использования "infer" для извлечения и переиспользования типаtype GetArrayElementType<T> = T extends (infer ElementType)[] ? ElementType : never;
type StringArray = string[];type Element = GetArrayElementType<StringArray>; // Element будет 'string'Этот подход не “кэширует” сам процесс вычисления типа компилятором, но позволяет тебе один раз определить сложный промежуточный тип и затем переиспользовать его по имени (MemoizedResult или ElementType), что улучшает читаемость и консистентность, а в некоторых случаях может помочь компилятору избегать повторных обходов AST, если он сможет разрешить тип быстрее по имени.
🚀 Оптимизация на уровне выполнения (Runtime)
Заголовок раздела «🚀 Оптимизация на уровне выполнения (Runtime)»Теперь переходим к классической оптимизации — как сделать наш код быстрее, когда он уже запущен. TypeScript здесь выступает как наш надежный помощник, который следит за типами и позволяет нам строить более эффективные и безопасные паттерны.
1. Type Guards для узких мест производительности
Заголовок раздела «1. Type Guards для узких мест производительности»Type Guards (защитники типов) — это не только про безопасность, но и про потенциальную оптимизацию. Когда компилятор сужает тип внутри блока if (например, if (typeof value === 'string')), это помогает ему “знать” больше о данных. На runtime это может позволить избежать лишних проверок или вызовов функций, которые не применимы к текущему типу.
interface ApiResponse { status: 'success' | 'error'; data?: unknown; // data может быть чем угодно message?: string;}
interface SuccessData { id: string; payload: string;}
function isSuccessData(data: unknown): data is SuccessData { return typeof data === 'object' && data !== null && 'id' in data && 'payload' in data;}
function processResponse(response: ApiResponse) { if (response.status === 'success' && response.data) { // Здесь response.data все еще unknown. Нужен Type Guard! if (isSuccessData(response.data)) { // TypeScript теперь знает, что response.data это SuccessData console.log(`Успешно обработаны данные: ID=${response.data.id}, Payload=${response.data.payload}`); // Здесь мы избежали ошибок на runtime и обеспечили безопасность // Если бы мы не использовали Type Guard, пришлось бы делать приведения типов (as), // что небезопасно и не даёт никаких гарантий. } else { console.warn("Получены успешные данные, но в неожиданном формате."); } } else if (response.status === 'error' && response.message) { console.error(`Ошибка: ${response.message}`); } else { console.warn("Неизвестный статус ответа или отсутствуют данные."); }}
processResponse({ status: 'success', data: { id: 'abc-123', payload: 'Some valuable info' } });processResponse({ status: 'error', message: 'Something went wrong' });processResponse({ status: 'success', data: 'malformed data' }); // Отработает через else ветку внутри2. Мемоизация функций/методов с декораторами или HOCs
Заголовок раздела «2. Мемоизация функций/методов с декораторами или HOCs»Мемоизация (caching) результатов дорогих вычислений — классический паттерн оптимизации runtime. TypeScript, благодаря декораторам и строгой типизации, делает этот паттерн очень элегантным.
// Простая функция мемоизации, кэширует результат по аргументамfunction memoize<T extends (...args: any[]) => any>( fn: T, cache: Map<string, ReturnType<T>> = new Map()): T { return function (...args: Parameters<T>): ReturnType<T> { const key = JSON.stringify(args); // Простой ключ, можно сделать более сложный if (cache.has(key)) { console.log(`CACHE HIT for ${fn.name || 'anonymous'} with args: ${key}`); return cache.get(key)!; } console.log(`CACHE MISS for ${fn.name || 'anonymous'} with args: ${key}`); const result = fn(...args); cache.set(key, result); return result; } as T;}
// Применяем мемоизацию к "дорогой" функцииconst expensiveCalculation = memoize(function calculateFactorial(n: number): number { if (n <= 1) return 1; let result = 1; for (let i = 2; i <= n; i++) { result *= i; } return result;});
console.log(expensiveCalculation(5)); // Вычисление, CACHE MISSconsole.log(expensiveCalculation(5)); // Из кэша, CACHE HITconsole.log(expensiveCalculation(7)); // Вычисление, CACHE MISSconsole.log(expensiveCalculation(5)); // Из кэша, CACHE HITДекоратор для мемоизации методов:
// Декоратор для мемоизации методаfunction MemoizeMethod() { const cache = new Map<string, any>(); // Кэш для результатов
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) { const key = JSON.stringify(args); // Ключ для кэша if (cache.has(key)) { console.log(`[DECORATOR] CACHE HIT for method ${propertyKey} with args: ${key}`); return cache.get(key); } console.log(`[DECORATOR] CACHE MISS for method ${propertyKey} with args: ${key}`); const result = originalMethod.apply(this, args); cache.set(key, result); return result; };
return descriptor; };}
class Calculator { @MemoizeMethod() add(a: number, b: number): number { console.log(`Performing heavy addition: ${a} + ${b}`); return a + b; }
@MemoizeMethod() multiply(a: number, b: number): number { console.log(`Performing heavy multiplication: ${a} * ${b}`); // Имитация дорогой операции for (let i = 0; i < 1000000; i++) {} return a * b; }}
const calc = new Calculator();console.log(calc.add(1, 2)); // Вычислениеconsole.log(calc.add(1, 2)); // Из кэшаconsole.log(calc.multiply(3, 4)); // Вычислениеconsole.log(calc.multiply(3, 4)); // Из кэша3. Иммутабельность и readonly
Заголовок раздела «3. Иммутабельность и readonly»Использование иммутабельных структур данных упрощает отладку, предотвращает нежелательные побочные эффекты и может косвенно улучшить производительность за счет более простых механизмов сравнения (ссылки вместо глубоких сравнений). TypeScript помогает обеспечить иммутабельность через модификатор readonly.
interface Task { readonly id: string; // ID нельзя изменить readonly title: string; readonly completed: boolean; readonly assignedTo?: readonly string[]; // Массив строк, которые нельзя изменить}
const myTask: Task = { id: "task-1", title: "Написать статью", completed: false, assignedTo: ["Яша", "Коллега"],};
// myTask.id = "new-id"; // Ошибка: Cannot assign to 'id' because it is a read-only property.// myTask.assignedTo.push("Еще кто-то"); // Ошибка: Property 'push' does not exist on type 'readonly string[]'.
// Для изменения задачи нужно создать новуюconst updatedTask: Task = { ...myTask, // Копируем все свойства completed: true, // Изменяем только нужные assignedTo: myTask.assignedTo ? [...myTask.assignedTo, "Новый человек"] : ["Новый человек"], // Обновляем массив};
console.log(myTask);console.log(updatedTask);😩 Типичные ошибки при оптимизации
Заголовок раздела «😩 Типичные ошибки при оптимизации»- Преждевременная оптимизация (Premature Optimization): Самая большая ошибка. Не оптимизируй то, что не является узким местом. Измеряй производительность (профилируй!) перед тем, как что-то менять.
- Игнорирование
readonly: Если данные не должны изменяться, используйreadonly. Это улучшает читаемость, безопасность и дает компилятору больше информации. - Слишком сложные типы: Избегай глубоко рекурсивных или слишком сложных условных типов, если они значительно замедляют компиляцию. Иногда простота важнее типовой “точности”.
- Слепое использование
any:anyубивает всю типобезопасность и, как следствие, потенциальные оптимизации и проверки, которые мог бы выполнить TypeScript.
### 🎯 Практика
Заголовок раздела «### 🎯 Практика»Время показать, что ты не просто слушаешь, а понимаешь!
- Создай декоратор
Debounce: Напиши декоратор для методов класса, который будет “откладывать” выполнение метода, пока не пройдет определенное время без новых вызовов. Типизируй его так, чтобы он работал с любыми аргументами и возвращаемыми значениями метода.@Debounce(500)должен откладывать вызов на 500мс.
- Утилита типа
DeepImmutable<T>: Создай утилиту типа, которая рекурсивно делает все свойства объекта и элементы массивовreadonly.- Пример использования:
type ImmutableConfig = DeepImmutable<MyConfig>;
- Пример использования:
- Оптимизация обработки событий с Type Guard: Предположим, у тебя есть общий обработчик событий
handleEvent(event: Event | CustomAPIEvent).CustomAPIEventимеет полеdetail: { type: 'login' | 'logout', data: any }. НапишиType GuardдляCustomAPIEventи используй его вhandleEventдля безопасной обработки.
### 💡 Совет
Заголовок раздела «### 💡 Совет»Помни, Яша всегда говорит: “Сначала сделай, чтобы работало. Потом сделай, чтобы было понятно. И только потом (если нужно) сделай, чтобы было быстро.” Оптимизация — это мощный инструмент, но его нужно применять с умом. TypeScript здесь не просто помогает писать код без ошибок, но и является ценным союзником в создании высокопроизводительных и поддерживаемых приложений.
Практика
Заголовок раздела «Практика»Попробуйте примеры в интерактивном редакторе: