Chupurnov Valeriy
Chupurnov Valeriy
Front End Engineer
Назад

Интересная техника уточнения примитивных типов в TypeScript

/blog/uploads/images/1631520340086-image-image.webp

В книге Эффективный TypeScript - Дэна Вандеркама, прочитал одну интересную технику для более строгой проверки примитивных типов данных. Которой хочу поделиться с вами. В TS как и в JS, есть примитивные типы данных. Эти типы не несут в себе никакой дополнительной мета информации, но иногда хочется ее иметь в самом типе.

К примеру:

const path: string = "/var/www/"; funcWorkOnlyWithAbs(path);
Copy

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

Мы можем ввести дополнительный метод:

function isAbsolute(path: string): boolean { return path.startsWith('/') }
Copy

И использовать его в своем коде

const path: string = "/var/www/"; if (isAbsolute(path)) { funcWorkOnlyWithAbs(path); }
Copy

Однако, переменная path все еще не типизирована, как абсолютный путь. В ней нет этой метаинформации, которая бы позволила ее однозначно идентифицировать.

Можно бы было ввести, что-то типа:

interface Path { path: string; } interface AbsolutePath extends Path { isAbsolute: true; } const path: AbsolutePath = { path: "/var/www/", isAbsolute: true } if (path.isAbsolute) { funcWorkOnlyWithAbs(path.path); }
Copy

Но первое - это неудобно, приходится везде таскать лишнюю абстракцию. Второе - это уже не string.

Так вот техника заключается а том, что мы можем создать номинальный тип данных, расширяя string. TS точно будет знать, что эта переменная не просто строка, а имеет какие-то дополнительные уточнения и мета информацию.

type AbsolutePath = string & {_brand: 'absolute'}; function isAbsolute(path: string): str is AbsolutePath { return path.startsWith('/') } if (isAbsolute(path)) { funcWorkOnlyWithAbs(path); }
Copy

В чем разница? В функции funcWorkOnlyWithAbs. До этого она имела такую сигнатуру:

function funcWorkOnlyWithAbs(path: string): void;
Copy

И ничего не мешало, вызвать ее так:

funcWorkOnlyWithAbs('./core/');
Copy

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

const path: string = "/var/www/"; function funcWorkOnlyWithAbs(path: AbsolutePath): void; if (isAbsolute(path)) { // path - точно содержит нужный нам путь funcWorkOnlyWithAbs(path); } funcWorkOnlyWithAbs('./core/'); // Ошибка
Copy

Техника может быть применена не только со строками:

type Meters = number & {_type: 'meter'}; type Miles = number & {_type: 'mile'}; const meters = (v: number): Meters => v; const miles = (v: number): Miles => v; function calculateDistance(a: Meters, b: Meters): Meters { return Math.abs(a - b); } const a = meters(100); const b = meters(10); console.log(calculateDistance(a, b));
Copy

Вы точно знаете какую единицу измерения возвращает calculateDistance, какую ждет в качестве аргументов. Это не нужно дополнительно обозначать в JSDoc.

Причем для JS не появилось лишних абстракций. Обычные числа. А ts стал строже и не позволит считать мили, там где требуются метры.

Вот такая интересная техника. Всем добра!