1

I'm new to typescript, but understand javascript. I'm building a simple utility class that will change the ASCII color of the terminal output. I want the function to be callable like this:

Colorize(ColorizeColors.Red).and(ColorizeBackgrounds.BrightBlue).on('hello world');

Ideally, I would like to be able to pass either a ColorizeColors or ColorizeBackground object to both the initial function call and the .and function and have it work either way. The .on function gets the string text and returns the colorized string. I have created this enums with the correct codes:

export enum ColorizeColors {
  Black = "\u001b[30m",
  Red = "\u001b[31m",
  Green = "\u001b[32m",
  Yellow = "\u001b[33m",
  Blue = "\u001b[34m",
  Magenta = "\u001b[35m",
  Cyan = "\u001b[36m",
  White = "\u001b[37m",
  BrightBlack = "\u001b[30;1m",
  BrightRed = "\u001b[31;1m",
  BrightGreen = "\u001b[32;1m",
  BrightYellow = "\u001b[33;1m",
  BrightBlue = "\u001b[34;1m",
  BrightMagenta = "\u001b[35;1m",
  BrightCyan = "\u001b[36;1m",
  BrightWhite = "\u001b[37;1m",
  Reset = "\u001b[0m",
}

export enum ColorizeBackgrounds {
  Black = "\u001b[40m",
  Red = "\u001b[41m",
  Green = "\u001b[42m",
  Yellow = "\u001b[43m",
  Blue = "\u001b[44m",
  Magenta = "\u001b[45m",
  Cyan = "\u001b[46m",
  White = "\u001b[47m",
  BrightBlack = "\u001b[40;1m",
  BrightRed = "\u001b[41;1m",
  BrightGreen = "\u001b[42;1m",
  BrightYellow = "\u001b[43;1m",
  BrightBlue = "\u001b[44;1m",
  BrightMagenta = "\u001b[45;1m",
  BrightCyan = "\u001b[46;1m",
  BrightWhite = "\u001b[47;1m",
  Reset = "\u001b[0m",
}

This is a quick try of some pseudo-Javascript code of the way that I would think it would be implemented, but I can't get it to work in real Typescript.

function Colorize (color: ColorizeColors | ColorizeBackgrounds) {
  if (typeof color === ColorizeColors) {
    this.fg = color;
  } else if (typeof color === ColorizeBackgrounds) {
    this.bg = color;
  }
  return this;
}

Colorize.and = function (color: ColorizeColors | ColorizeBackgrounds) {
  if (typeof color === ColorizeColors) {
    this.fg = color;
  } else if (typeof color === ColorizeBackgrounds) {
    this.bg = color;
  }
  return this;
}

Colorize.on = function (text: string) {
  return `${this.bg}${this.fg}${text}${ColorizeColors.Reset}`;
}

I have gotten the function to work correctly with other styles of calling it, such as

Colorize.text(ColorizeColors.Red).bg(ColorizeBackgrounds.BrightBlue).on('hello world')

but I like the first style better and am trying to understand how to produce it as an exercise.

1
  • 1
    At runtime ColorizeColors.Reset and ColorizeBackgrounds.Reset are the same so you can't figure out if it's foreground or background. Is that important? If I call Colorize(ColorizeColors.Reset) what is supposed to happen? Commented Jul 3, 2020 at 2:58

1 Answer 1

1

In order for your plan to work you need to make sure that all the foreground and background color values are distinct strings at runtime. That looks to be the case for your normal colors, but Reset is the same for both. I'm going to move Reset out of the enums. For simplicity, here's the data I'm using:

enum FgColors {
    Red = "[fgRed]",
    Green = "[fgGrn]",
    Blue = "[fgBlu]"
}
enum BgColors {
    Red = "[bgRed]",
    Green = "[bgGrn]",
    Blue = "[bgBlu]"
}
const Reset = "[reset]";

You can't use typeof at runtime to determine if a color is FgColors or BgColors because they will just be strings, and typeof color will be "string". Instead you will have to look for the particular string inside the values of the FgColors enum object, like this:

function isFgColor(val: FgColors | BgColors): val is FgColors {
    return (Object.values(FgColors) as ReadonlyArray<typeof val>).includes(val);
}

Then your Colorize() function could possibly be implemented like this:

function Colorize<T extends FgColors | BgColors>(color1: T) {
    return {
        and(color2: T extends FgColors ? BgColors : FgColors) {
            return {
                on(text: string) {
                    const [fg, bg] = isFgColor(color1) ? [color1, color2] : [color2, color1];
                    return `${bg}${fg}${text}${Reset}`;
                }
            }
        }
    }
};

By having the function be generic, we can make the compiler try to enforce that the call to and() should pick a color from the other enum. The generic type T corresponds to the first color, and T extends FgColors ? BgColors : FgColors is a conditional type that says "if T is a FgColors, then we want a BgColors; otherwise we want a FgColors. This isn't strictly what you asked for, but without it, you run the risk of allowing two foregrounds or two backgrounds.

Notice how the Colorize() function is not making a class or using this; instead, it returns an object whose single and property is a function that returns an object whose single on property is a function that returns a string.

Let's test it:

const test = Colorize(BgColors.Green).and(FgColors.Blue).on("hey");
console.log(test); // [bgGrn][fgBlu]hey[reset]
const test2 = Colorize(FgColors.Blue).and(BgColors.Green).on("you");
console.log(test2); // [bgGrn][fgBlu]you[reset]

That looks right, I think. And let's make sure you can't do it wrong:

const bad = Colorize(FgColors.Red).and(FgColors.Red); // error!
// ----------------------------------> ~~~~~~~~~~~~
// Argument of type 'FgColors.Red' is not assignable to parameter of type 'BgColors'

Looks good.

Okay, hope that helps; good luck!

Playground link to code

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks so much! I don't understand everything going on here yet, but going through this will be a big help in learning the language. As you said, Reset doesn't need to be in those enums or accessible outside the module, so it should just be a const. I wasn't quite clear on the spec, as I intended for the "and" function to be optional, but that will be a good exercise for me.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.