Код
Рассмотрим следующий код, один базовый класс, два подкласса и функцию, использующую один экземпляр одного подкласса.
abstract class AbstractNumberHolder {
constructor(private readonly value: number) { }
getValue() { return this.value; }
}
class UserId extends AbstractNumberHolder {
equals(u: UserId) { return this.getValue() === u.getValue(); }
}
class SubscriptionCost extends AbstractNumberHolder {
equals(s: SubscriptionCost) { return this.getValue() === s.getValue(); }
}
// ~~~
function printUserId(u: UserId) {
console.log(u.getValue());
}
// ~~~
const subCost = new SubscriptionCost(50);
const uid = new UserId(1000105);
printUserId(subCost); // DOES NOT ERROR, WHILE IT SHOULD!
( Typescript Playground link Код выше )
Объяснение
У меня есть два совершенно разных класса, UserId
и SubscriptionCost
, которые представляют два совершенно разных понятия. Поскольку их внутренняя реализация в настоящее время похожа (обе содержат числовые значения), я заставляю их расширять базовый класс AbstractNumberHolder
.
Проблема
Проблема есть, Typescript сравнивает только структуру классов, и эти два класса имеют одинаковую структуру! И это отстой ...
Это полностью сводит на нет причину, по которой я решил сначала выбрать Typescript. Если разработчик может случайно передать «Стоимость подписки» вместо «Идентификатор пользователя» "к функции, и машинопись не может ее поймать, тогда зачем вообще беспокоиться о наличии типов?!
Существующие неоптимальные решения
Я оглянулся и подумал о возможном решения для этого. Ниже приведены решения, которые могут работать, но я не был бы доволен ни одним из них.
Решение 1) Проверка вручную с использованием instanceof
Слава Богу, instanceof
работает как положено ! Итак, первая строка моего printUserId
может быть такой:
function printUserId(u: UserId) {
assert(u instanceof UserId, "Please pass a UserId!");
console.log(u.getValue());
}
Очевидно, что она вообще не масштабируется, и для каждого аргумента каждой функции мне нужно написать одну такую строку!
Решение 2) Используйте Exact
тип в функции
Я обнаружил, что существует множество творческих (но ручных и нестандартных) способов, которыми вы можете заставить тип быть точно тем, что вы хотеть. (например, комментарии этой проблемы github .)
Используя это, я могу заставить функцию принимать точно идентификатор пользователя.
type ExactInner<T> = <D>() => (D extends T ? D : D);
type Exact<T> = ExactInner<T> & T;
function printUserId(u: Exact<UserId>) {
console.log(u.getValue());
}
Это намного короче, чем первое решение, но все же мне нужно помнить, чтобы поставить Exact<...>
на каждый типизированный аргумент каждой функции в моем коде!
Решение 3) Добавить нюанс к классам
Одним из основных недостатков предыдущих решений является необходимость изменения функций , которые используют экземпляры этих классов. И вообще говоря, в коде больше функций, чем классов.
Итак, пытаясь решить эту проблему с другой точки зрения, я могу добавить нюанс к классам, чтобы сделать их неизменяемыми!
class UserId extends AbstractNumberHolder {
private I_AM_A_USER_ID = true;
}
class SubscriptionCost extends AbstractNumberHolder {
private I_AM_A_SUBSCRIPTION_COST = true;
}
Опять же, это решение требует ручного изменения для каждого класса и выглядит избыточным. (Это также плохо масштабируется с классами, напрямую поступающими с фабрики классов.)
Решение 4) Добавить Private Nuance во время extend
(Ароматизатор / Брендинг)
Я нашел это замечательная статья в https://michalzalecki.com/nominal-typing-in-typescript/, рассказывающая о нашей проблеме номинального набора текста. Кроме того, https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/ отлично сравнивает два тонких метода брендинга.
Основываясь на найденных там решениях, я могу подумать о передаче строки (как фальшивого типа) в родительский класс и делая из этого нюанс / бренд / вкус.
abstract class AbstractNumberHolder<T extends string> {
private _?: T;
constructor(private readonly value: number) { }
getValue() { return this.value; }
}
class UserId extends AbstractNumberHolder<"UserId"> {
equals(u: UserId) { return this.getValue() === u.getValue(); }
}
class SubscriptionCost extends AbstractNumberHolder<"SubscriptionCost"> {
equals(s: SubscriptionCost) { return this.getValue() === s.getValue(); }
}
Что мне нравится в этом решении, так это то, что мне нужно внести изменения, когда я добавляю больше классов / функций, минимально. Кроме того, вы не пропустите необходимые изменения (так как T extends string
не является обязательным).
Однако здесь все еще есть ручная работа. Я могу sh это может быть автоматизировано ...
Итак, у любого есть лучшее решение для этой проблемы, которое гарантирует, что ни один разработчик не упустит случайно (при создании большего количества классов / функций в будущем ) и не требует ручной работы для каждого класса / функции?