javascript

Как заставить TS работать на вас

  • пятница, 11 апреля 2025 г. в 00:00:04
https://habr.com/ru/companies/sportmaster_lab/articles/899546/

Привет! Меня зовут Дмитрий, и я уже много лет работаю с TypeScript. За это время я был частью разных команд с разным уровнем владения этим языком, в том числе тех, кто только готовился перевести проект с JavaScript. И нередко я замечал, что разработчики воспринимают TypeScript не как инструмент, упрощающий работу, а как рутинную обязанность, которая лишь замедляет процесс. В этой статье я расскажу, как сделать 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);
  });
});

Таким образом, мы можем извлечь сразу двойную пользу: получать подсказки при написании тестов и написании исполняемого кода в дальнейшем.

Прокачка TS

Работа с типами, как и с исполняемым кодом, со временем превращается в рутину. Количество повторяющихся операций растет, раздувая кодовую базу и увеличивая когнитивную нагрузку на команду.

Однако, если при написании исполняемого кода мысль использовать стороннюю библиотеку приходит почти сразу, то при работе с типами это чаще не происходит вообще.

Хорошая новость в том, что можно значительно облегчить жизнь команды, просто воспользовавшись специализированными библиотеками для работы с типами. Давайте разберемся, какие библиотеки существуют и как они могут помочь.

Исправления встроенных типов

Некоторые встроенные типы в TypeScript небезопасны или неудобным — так сложилось исторически. К счастью, есть библиотеки, которые исправляют эти недочеты:

  1. ts-reset

  2. types-spring

Пересказывать их документацию смысла нет — она и так очень маленькая, лучше ознакомиться с ней самостоятельно по ссылкам выше.

Дополнительные утилиты

Несмотря на то, что из коробки TS имеет широкий набор утилит, со временем его оказывается недостаточно и разработчики начинают комбинировать утилиты в сложные конструкции и далеко не всегда они надежны.

Чтобы облегчить команде жизнь, можно воспользоваться специализированными библиотеками.

Чтобы избежать этого и сделать код более выразительным, можно воспользоваться специализированными библиотеками.

Популярные библиотеки

  1. ts-toolbelt

  2. type-fest

  3. utility-types

  4. sniptt/guards


Если хочется сравнить их подробнее, рекомендую статью на Хабре, которая отлично раскрывает эту тему.

Но, чтобы показать, насколько могут быть полезны продвинутые утилиты, разберем пример на основе 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, а не боритесь с ним.