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);

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

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

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

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

const path: string = "/var/www/";

if (isAbsolute(path)) {
    funcWorkOnlyWithAbs(path);
}

Однако, переменная 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);
}

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

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

type AbsolutePath = string & {_brand: 'absolute'};

function isAbsolute(path: string): str is AbsolutePath {
 return path.startsWith('/')
}

if (isAbsolute(path)) {
    funcWorkOnlyWithAbs(path);
}

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

function funcWorkOnlyWithAbs(path: string): void;

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

funcWorkOnlyWithAbs('./core/');

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

const path: string = "/var/www/";

function funcWorkOnlyWithAbs(path: AbsolutePath): void;

if (isAbsolute(path)) {
     // path - точно содержит нужный нам путь
    funcWorkOnlyWithAbs(path);
}

funcWorkOnlyWithAbs('./core/'); // Ошибка

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

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));

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

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

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