Typescript: как ожидать точный экземпляр класса в качестве параметра функции - PullRequest
2 голосов
/ 10 февраля 2020

Код

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

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 это может быть автоматизировано ...


Итак, у любого есть лучшее решение для этой проблемы, которое гарантирует, что ни один разработчик не упустит случайно (при создании большего количества классов / функций в будущем ) и не требует ручной работы для каждого класса / функции?

...