Я создаю рисовальщик, который может объединять геттеры в цепочку, но каждый геттер можно вызвать только один раз. Для 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 они близки к тому, чего я хочу достичь, но они не добытчики.
Просто if (this.styles.includes("bold")) throw new Error()
? Или вы ищете ошибки типа во время компиляции?
Я думаю, вы можете напрямую применить подход из этого ответа, который вы нашли. Являются ли они геттерами или методами-геттерами, не должно иметь большого значения.
@jcalz Да, это именно то, что я хочу, пожалуйста, добавьте это к ответу, я приму это. Если бы вы могли добавить какие-то пояснения, было бы хорошо. Спасибо!
Я сделаю это, когда у меня будет возможность.
Что здесь происходит с правками? Есть ли причина, по которой мы меняем italics
на italic
, продолжая использовать "italics"
в styles
? Конечно, это не имеет ничего общего с примером, не так ли? Это заставляет меня редактировать мой незавершенный ответ, чтобы не отставать, и я не уверен, в чем смысл.
В качестве альтернативы вы можете избежать использования дочерних классов для проверки типов и просто использовать общий 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, я обнаружил, что твое решение на самом деле то, что я искал вначале. Поскольку я не знал, как это сделать, я придумал решения для подклассов. Но твой более элегантный. Но я уже принимаю ответ Джкалца, и он больше относится к этому вопросу. Хотите, чтобы я создал еще один вопрос, и вы могли бы скопировать туда ответ?
Я создал один: stackoverflow.com/questions/78560317/…
@Арст лол, оно закрыто
О, эй, @Arst, ты не принял мой ответ, несмотря на то, что я проверил у тебя, соответствует ли типизация твоего кода тому, что ты искал. Этот рефакторинг может быть лучше для вашего основного варианта использования, но это не то, о чем вы просили. Вы имеете право решить, что другой ответ лучше, и не принять мой, но... нужно ли мне было что-то сделать по-другому, чтобы избежать этой ситуации?
@jcalz, не переживай, меня много раз не принимали, и на меня не было никаких жалоб. в конце концов, цель состоит в том, чтобы найти лучшее решение, несмотря на исходный вопрос. как мои извинения, примите мой голос, пожалуйста, спасибо
@Arst, когда я опубликовал свой ответ, была глубокая ночь, и мне не удалось скопировать последнюю версию с игровой площадки. Я обновил свой ответ и удалил прокси-сервер конструктора, поскольку вы используете статический фабричный метод для создания художника, и вам не нужно поддерживать ключевое слово new
.
@AlexanderNenachev, тебе не нужно извиняться, и Арсту тоже не нужно, но спасибо за ответ.
Вам нужно 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>
. Его свойство bold
— StyleDecoratorWithour<"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 возможно? Но это отступление от заданного вопроса.
Соответствует ли такой подход вашим потребностям? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?