Интересная техника уточнения примитивных типов в TypeScript
В книге Эффективный 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
стал строже и не позволит считать мили, там где требуются метры.
Вот такая интересная техника. Всем добра!