Предотвратите двойной вызов одного и того же геттера с помощью связанных геттеров

Я создаю рисовальщик, который может объединять геттеры в цепочку, но каждый геттер можно вызвать только один раз. Для TextDecorator и BgDecorator с ними легко справиться, потому что я могу просто вернуть разные классы. Однако для StyleDecorator, поскольку разрешено применять несколько стилей, я не знаю, как определить тип, который может накапливать пропущенные геттеры. Пожалуйста, смотрите код ниже:

class Painter {
  protected textColor: string
  protected bgColor: string
  protected styles: string[]

  paint() {
    // paint the text
  }
  static create() {
    return new TextDecorator()
  }
}

class StyleDecorator extends Painter  {
  get bold(): Omit<StyleDecorator, 'bold'> {
    this.styles.push('bold')
    return this
  }
  get underline(): Omit<StyleDecorator, 'underline'> {
    this.styles.push('underline')
    return this
  }
  get italic(): Omit<StyleDecorator, 'italic'> {
    this.styles.push('italics')
    return this
  }
}

class BgDecorator extends StyleDecorator {
  get redBg(): StyleDecorator {
    this.bgColor = "red"
    return this
  }
  get blueBg(): StyleDecorator {
    this.bgColor = "blue"
    return this
  }
}

class TextDecorator extends BgDecorator {
  get white(): BgDecorator {
    this.textColor = "white"
    return this
  }
  get black(): BgDecorator {
    this.textColor = "black"
    return this
  }

}



Painter.create().white.redBg.bold.underline.paint() // ok
Painter.create().white.blueBg.bold.underline.bold.paint() // I want to see error that says bold does not exist.
Painter.create().white.redBg.underline.bold.underline.paint() // I want to see error that says underline does not exist.
Painter.create().white.redBg.italic.underline.italic.underline.paint() // I want to see error that says underline and italic do not exist.

Я посмотрел это от lukasgeiter и это от Titain они близки к тому, чего я хочу достичь, но они не добытчики.

Соответствует ли такой подход вашим потребностям? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?

jcalz 29.05.2024 17:31

Просто if (this.styles.includes("bold")) throw new Error()? Или вы ищете ошибки типа во время компиляции?

Bergi 29.05.2024 17:43

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

Bergi 29.05.2024 17:46

@jcalz Да, это именно то, что я хочу, пожалуйста, добавьте это к ответу, я приму это. Если бы вы могли добавить какие-то пояснения, было бы хорошо. Спасибо!

Arst 30.05.2024 01:33

Я сделаю это, когда у меня будет возможность.

jcalz 30.05.2024 01:50

Что здесь происходит с правками? Есть ли причина, по которой мы меняем italics на italic, продолжая использовать "italics" в styles? Конечно, это не имеет ничего общего с примером, не так ли? Это заставляет меня редактировать мой незавершенный ответ, чтобы не отставать, и я не уверен, в чем смысл.

jcalz 30.05.2024 02:16
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
1
6
97
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

В качестве альтернативы вы можете избежать использования дочерних классов для проверки типов и просто использовать общий Proxy для всех ваших цветов и стилей, более того, вы можете ввести свойства Painter вместо использования string.

Это также позволяет вам вводить стили и цвета в любом порядке (хороший или плохой).

Хорошо, что вы можете определить цвета по умолчанию (например, белый фон и черный текст) и установить только стили текста. Я думаю, что это более гибко, чем использование дочерних классов, где вам НЕОБХОДИМО установить цвет для установки фона. Кажется, это неприятное ограничение.

Машинописная площадка

const colors = ['white', 'black', 'red', 'blue'] as const;
const styles = ['underline', 'bold', 'italic'] as const;

type Color = typeof colors[number];
type BgColor = keyof { [K in Color as `${K}Bg`]: never };
type Style = typeof styles[number];

// sets for faster lookups
const colorSet = new Set(colors);
const bgColorSet = new Set(colors.map(c => `${c}Bg`) as BgColor[]);
const styleSet = new Set(styles);

type AllStyles = Color | BgColor | Style;

type Decorator<T extends AllStyles = never> = Omit<{
    [K in Style]: Omit<Decorator<T | K>, K>
}, T> & Omit<{
    [K in Color]: Omit<Decorator<T | Color>, Color>
}, T> & Omit<{
    [K in BgColor]: Omit<Decorator<T | BgColor>, BgColor>
}, T> & 
Painter;

class Painter {
    protected textColor: Color | '' = ""
    protected bgColor: Color | '' = ""
    protected styles: Style[] = []

    paint() {
        console.info(this);
    }
    static create(): Decorator { 
        return new Proxy<Decorator>(new Painter as Decorator, {
            get(target, prop, proxy){               
                if (colorSet.has(prop as Color)){
                    target.textColor = prop as Color;
                }else if (bgColorSet.has(prop as BgColor)){
                    target.bgColor = prop as Color;
                }else if (styleSet.has(prop as Style)){
                    target.styles.push(prop as Style);
                }else {
                    return Reflect.get(target, prop, proxy);
                }
                return proxy;
            }
        });

    }
}

Painter.create().white.black.redBg.bold.underline.paint() // black is an error
Painter.create().white.blueBg.bold.underline.bold.paint() // I want to see error that says bold cannot be called twice.
Painter.create().white.redBg.underline.bold.underline.paint() // I want to see error that says underline cannot be called twice.
Painter.create().white.redBg.italic.underline.italic.underline.paint() // I want to see error that says underline and italics cannot be called twice.

Привет @Alex, я обнаружил, что твое решение на самом деле то, что я искал вначале. Поскольку я не знал, как это сделать, я придумал решения для подклассов. Но твой более элегантный. Но я уже принимаю ответ Джкалца, и он больше относится к этому вопросу. Хотите, чтобы я создал еще один вопрос, и вы могли бы скопировать туда ответ?

Arst 31.05.2024 14:44

Я создал один: stackoverflow.com/questions/78560317/…

Arst 31.05.2024 15:27

@Арст лол, оно закрыто

Alexander Nenashev 31.05.2024 16:59

О, эй, @Arst, ты не принял мой ответ, несмотря на то, что я проверил у тебя, соответствует ли типизация твоего кода тому, что ты искал. Этот рефакторинг может быть лучше для вашего основного варианта использования, но это не то, о чем вы просили. Вы имеете право решить, что другой ответ лучше, и не принять мой, но... нужно ли мне было что-то сделать по-другому, чтобы избежать этой ситуации?

jcalz 31.05.2024 17:13

@jcalz, не переживай, меня много раз не принимали, и на меня не было никаких жалоб. в конце концов, цель состоит в том, чтобы найти лучшее решение, несмотря на исходный вопрос. как мои извинения, примите мой голос, пожалуйста, спасибо

Alexander Nenashev 31.05.2024 17:43

@Arst, когда я опубликовал свой ответ, была глубокая ночь, и мне не удалось скопировать последнюю версию с игровой площадки. Я обновил свой ответ и удалил прокси-сервер конструктора, поскольку вы используете статический фабричный метод для создания художника, и вам не нужно поддерживать ключевое слово new.

Alexander Nenashev 31.05.2024 17:47

@AlexanderNenachev, тебе не нужно извиняться, и Арсту тоже не нужно, но спасибо за ответ.

jcalz 31.05.2024 20:24

Вам нужно StyleDecorator отслеживать, какие свойства уже были опущены. В противном случае вы просто забудете все, кроме самой последней операции. Это предотвратило бы painter.bold.bold, но не остановило бы painter.bold.underline.bold.

Вы хотите, чтобы свойство bold объекта StyleDecorator имело тип Omit<StyleDecorator, "bold" | K>, где K — это то, что вы уже пропустили. Это означает, что StyleDecorator должен быть общим в K, а это означает, что каждый раз, когда вы опускаете , вам также придется добавить имя текущего вызываемого метода к K через объединение.

Это выглядит так:

type StyleDecoratorWithout<K extends PropertyKey> = Omit<StyleDecorator<K>, K>;

class StyleDecorator<K extends PropertyKey = never> extends Painter {
    get bold() {
        this.styles.push('bold')
        return this as StyleDecoratorWithout<K | "bold">
    }
    get underline() {
        this.styles.push('underline')
        return this as StyleDecoratorWithout<K | "underline">
    }
    get italic() {
        this.styles.push('italic')
        return this as StyleDecoratorWithout<K | "italic">
    }
}

Painter.create().white.redBg.bold.underline.paint() // ok
Painter.create().white.blueBg.bold.underline.bold.paint() // error
Painter.create().white.redBg.underline.bold.underline.paint() // error
Painter.create().white.redBg.italic.underline.italic.underline.paint() // error

Итак, Painter.create().white относится к типу StyleDecorator<never>. Его свойство boldStyleDecoratorWithour<"bold">, что означает, что он не имеет свойства "bold" (поскольку мы Omit установили "bold") и что любое из его других свойств будет включать "bold" в список вещей, которые следует опустить. Тогда его свойством underline является StyleDecoratorWithout<"bold" | "underline">, поэтому у него нет свойств "bold" и "underline". После этого вы можете получить доступ к свойству italic, которое StyleDecoratorWithout<"bold" | "underline" | "italic"> не имеет ни одного из этих трех свойств. Вам фактически запрещено легко получать доступ к одному и тому же свойству несколько раз в цепочке получения.


Это ответ на заданный вопрос.

Но учтите, что на самом деле это не мешает вам многократно обращаться к одному и тому же свойству без цепочки:

const p = Painter.create().white
p.bold;
p.bold;
console.info(p["styles"]) // ["bold", "bold"], oops

И если вам нужно предотвратить это, то вы обнаружите, что это практически невозможно в TypeScript (поскольку для этого потребуется отслеживать произвольное изменяемое состояние данной переменной, чего TypeScript не может сделать). Я бы рекомендовал вам убедиться фактический код может обрабатывать несколько обращений к свойству (скажем, не push() определяя, если оно уже существует; используйте Set возможно? Но это отступление от заданного вопроса.

Детская площадка, ссылка на код

Другие вопросы по теме