Как заставить TS работать на вас
- пятница, 11 апреля 2025 г. в 00:00:04
Привет! Меня зовут Дмитрий, и я уже много лет работаю с TypeScript. За это время я был частью разных команд с разным уровнем владения этим языком, в том числе тех, кто только готовился перевести проект с JavaScript. И нередко я замечал, что разработчики воспринимают TypeScript не как инструмент, упрощающий работу, а как рутинную обязанность, которая лишь замедляет процесс. В этой статье я расскажу, как сделать TypeScript своим союзником и заставить его работать на вас, а не против.
Чтобы понять, откуда берётся большинство проблем, начнём с самого начала. Почти все программисты переходят на TypeScript после долгой работы с JavaScript, сформировав определённые привычки и устоявшиеся практики.
Главная проблема здесь, на мой взгляд, в том, что многие пытаются просто наложить TypeScript поверх JavaScript-кода стараясь удовлетворить требованиям назойливого компилятора, вместо того чтобы писать на TS изначально.
Дальше мы разберёмся, как избавиться от старых привычек и научиться писать код так, чтобы TypeScript действительно облегчал работу.
Одна из распространенных ошибок разработчиков, переходящих с JavaScript на TypeScript, — игнорирование типизации на этапе написания кода. Вместо того чтобы заранее описывать ожидаемые данные, многие полагаются исключительно на автоматическое выведение типов (type inference), что может приводить к неожиданным ошибкам.
Последовательно рассмотрим два подхода к разработке:
1. Классический — пишем исполняемый код, не задумываясь о типах;
2. TypeScript-ориентированный — определяем требования к типам, затем реализуем логику.
Допустим, мы разрабатываем компонент, который должен корректно обрабатывать ширину элемента так же, как это сделано, например, в компоненте VCard из Vuetify:
<VCard width="100" />
<VCard width="100px" />
<VCard width="100%" />
Начнем с привычного подхода — просто напишем код без явного указания типов:
function defineElementWidth(value: string) {
// Тип не проверяется 😱
return {
width: `${value}рх`, // ❌ На самом деле px - кириллица 🤬
};
}
// ❌ Можно передать заведомо невалидное значение
defineElementWidth('Не число');
TypeScript не подсвечивает ошибку, потому что он не понимает, какие данные мы ожидаем. В результате неправильное значение спокойно проходит, и даже опечатка остается незамеченной.
Теперь попробуем задать строгие ограничения:
// Явно указываем тип аргумента и возвращаемого значения
function defineElementWidth(value: number): { width: `${number}px` } {
// Типы проверяются на соответствие ожидаемым
return {
width: `${value}px`, // ✅
};
}
// Ошибки обнаруживаются на этапе написания кода
defineElementWidth('Не число'); // ❌
defineElementWidth(100); // ✅
Теперь функция защищена от ошибок, разработчик может быть абсолютно уверен, что реализация соответствует его ожиданиям.
Добавим поддержку значений в px и в %, как во Vuetify:
// TypeScript сразу покажет ошибку в реализации
function defineElementWidth(value: number | `${number}%` | `${number}px` ): { width: `${number}${'px' | '%'}` } {
return {
width: `${value}px`, // ❌ Ошибка! value уже может содержать px или %
}
}
TypeScript мгновенно подскажет, что возвращаемый тип не соответствует ожидаемому без запуска кода и долгой отладки.
Финальная реализация
Теперь создадим универсальное решение, которое корректно обрабатывает все допустимые значения:
type Unit = `px` | '%';
type Width = number | `${number}` | `${number}${Unit}`;
type Style = { width: `${number}${Unit}` };
function defineElementWidth(value: Width): Style {
if (typeof value === 'string') {
const match = value.match(/^(\d+)([a-zA-Z%]*)$/)?.slice(1).filter(Boolean) ?? [];
const [width = '0', unit = 'px'] = match;
return {
width: `${parseFloat(width)}${unit as Unit}`,
};
}
return {
width: `${value}px`,
};
}
Обратите внимание, что мы сначала разобрались с типами, а потом шаг за шагом реализовали каждый из них получая подсказки на каждом шаге.
Теперь функция корректно обрабатывает числа, строки и значения с единицами измерения, возвращает предсказуемый и строго типизированный объект, не допускает ошибок благодаря встроенной проверке типов.
TypeScript работает эффективнее, если мы сначала определяем типы, а затем реализуем логику. Это позволяет получать подсказки от IDE прямо во время написания функции.
Таким подходом лучше всего проникнуться на практике, попробуйте самостоятельно написать аналогичную функцию и внимательно следите за тем, как TypeScript помогает вам на каждом этапе: сначала определите все возможные сценарии использования функции, и только потом напишите исполняемый код.
Если вы знакомы с методологией Test Driven Development (TDD), то, возможно, заметили сходство, TDD предлагает похожий подход - сначала написать тесты и только потом исполняемый код.
Этим подходы отлично дополняют друг друга, мы можем сначала определить типы и написать тесты, не имея готовой реализации..
Однако TypeScript потребует, чтобы функция была реализована, поэтому можно использовать заглушку:
// Заглушка позволяет не выдавать ошибки пока нет реализации
function foo(): 'Foo' {
throw new Error('foo() is not implemented');
}
// Тест проверяет ожидаемый тип результата
describe('foo()', () => {
it('foo() возвращает "Foo"', () => {
const expected: ReturnType<typeof foo> = 'Foo';
expect(foo()).toBe(expected);
});
});
Таким образом, мы можем извлечь сразу двойную пользу: получать подсказки при написании тестов и написании исполняемого кода в дальнейшем.
Работа с типами, как и с исполняемым кодом, со временем превращается в рутину. Количество повторяющихся операций растет, раздувая кодовую базу и увеличивая когнитивную нагрузку на команду.
Однако, если при написании исполняемого кода мысль использовать стороннюю библиотеку приходит почти сразу, то при работе с типами это чаще не происходит вообще.
Хорошая новость в том, что можно значительно облегчить жизнь команды, просто воспользовавшись специализированными библиотеками для работы с типами. Давайте разберемся, какие библиотеки существуют и как они могут помочь.
Некоторые встроенные типы в TypeScript небезопасны или неудобным — так сложилось исторически. К счастью, есть библиотеки, которые исправляют эти недочеты:
Пересказывать их документацию смысла нет — она и так очень маленькая, лучше ознакомиться с ней самостоятельно по ссылкам выше.
Несмотря на то, что из коробки TS имеет широкий набор утилит, со временем его оказывается недостаточно и разработчики начинают комбинировать утилиты в сложные конструкции и далеко не всегда они надежны.
Чтобы облегчить команде жизнь, можно воспользоваться специализированными библиотеками.
Чтобы избежать этого и сделать код более выразительным, можно воспользоваться специализированными библиотеками.
Если хочется сравнить их подробнее, рекомендую статью на Хабре, которая отлично раскрывает эту тему.
Но, чтобы показать, насколько могут быть полезны продвинутые утилиты, разберем пример на основе ts-toolbelt — библиотеки, которую мы используем в нашей команде.
Допустим, у нас есть объект Person, и нам нужно создать его версию, где некоторые поля становятся необязательными:
// Из такого
type Person = {
name: string;
age: number;
salary: number;
}
// Сделать такой
type Person = {
name: string;
age?: number;
salary?: number;
}
Решим эту задачу, используя только стандартные утилиты:
type PersonOptional = Omit<Person, 'age' | 'salary'> & Partial<Pick<Person, 'age' | 'salary'>>;
В глаза сразу бросаются количество требуемых утилит, вложенность одной в другую и, особенно, дублирование имен ключей.
Теперь попробуем решить ту же задачу с помощью продвинутых утилит:
import { Object } from ‘ts-toolbelt’;
type PersonOptional = Object.Optional<Person, 'age' | 'salary'>;
Код стал значительно короче, а главное — понятнее.
Если даже в таком простом случае разница ощутима, то при работе с по- настоящему сложными типами выгода становится еще более заметной.
Если позволить TypeScript помочь вам, он станет незаменимым инструментом, без которого вы уже не сможете представить свою работу.
Программируйте на TS, а не боритесь с ним.