Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert casted values #25

Closed
panoply opened this issue Jan 11, 2025 · 14 comments
Closed

Revert casted values #25

panoply opened this issue Jan 11, 2025 · 14 comments
Labels
documentation Improvements or additions to documentation enhancement New feature or request
Milestone

Comments

@panoply
Copy link

panoply commented Jan 11, 2025

Current behaviour

As of 3.7.0 the change to casted values is returning undefined which is not ideal and problematic with implementations where primitives should not be stringified.

Expected behaviour

Result should be empty string.

Reproduction Example

In the below example, foo is undefined, but it is not ansis job to cast that, failing silently is far better imo as per the previous versions. The argument for type checking before printing can also be a nuance because it is assumed behaviour based on alternative libs.

import ansis from 'ansis';

export const { cyan } = ansis

// assuming i have a function
const someFn = (foo) =>  cyan(foo)

someFn() // undefined is printed

Additional context

While the logic is good in theory, in practice it is not ideal.

@webdiscus webdiscus added this to the analysis milestone Jan 11, 2025
@webdiscus
Copy link
Owner

webdiscus commented Jan 11, 2025

Hello @panoply,

thanks for the issue!

... but it is not ansis job to cast that

I wanted to get the same result as chalk and picocolors for "compatibility".

We can discuss the behaviour of all edge cases to work out the most optimal and expected result.

Currently behaviour:

  • ansis.green() - '\x1b[32mundefined\x1b[39m'
  • ansis.green(undefined) - '\x1b[32mundefined\x1b[39m' <= the same behaviour by chalk and picocolors
  • ansis.green(null) - '\x1b[32mnull\x1b[39m'
  • ansis.green(NaN) - '\x1b[32mNaN\x1b[39m'
  • ansis.green(Infinity) - '\x1b[32mInfinity\x1b[39m'
  • ansis.green(true) - '\x1b[32mtrue\x1b[39m'
  • ansis.green(false) - '\x1b[32mfalse\x1b[39m'
  • ansis.green('') - ''

See the test/functional.test.js

Your proposals (please complete)

  • ansis.green() - ''
  • ansis.green(undefined) - ''
  • ... ?

P.S.

Any PR is wellcome!

@webdiscus webdiscus added documentation Improvements or additions to documentation enhancement New feature or request labels Jan 11, 2025
@webdiscus
Copy link
Owner

webdiscus commented Jan 11, 2025

However using a template string, undefined will be casted into string undefined:

let foo; // undefined
console.log(`Hello ${foo}!`); // => Hello undefined!

Therefore would be best keep the same behaviour for consistency:

import { red, green } from 'ansis';
let foo; // undefined
console.log(green`Hello ${red(foo)}!`); // => \x1b[32mHello \x1b[31mundefined\x1b[32m!\x1b[39m

@webdiscus
Copy link
Owner

webdiscus commented Jan 11, 2025

Both variants are valid. It would be good to conduct a survey on stackoverflow and reddit.

@webdiscus webdiscus added help wanted Extra attention is needed question Further information is requested labels Jan 11, 2025
@webdiscus
Copy link
Owner

I will try to explain the changes.

Expected behaviour for developer-friendliness

  1. Rendering undefined as a String

    If a value like undefined or null is passed, it should convert the value to its string representation ('undefined' or 'null') and apply the color. This makes debugging easier since you can see exactly what value is being processed.
let foo; // undefined
console.log(ansis.red(foo)); // => 'undefined' in red
  1. Interpolation in Strings

    When interpolating undefined values inside template strings, it should be rendered as 'undefined'. This provides clarity and avoids silent errors where placeholders appear empty.
let foo; // undefined
console.log(ansis.green(`Hello ${ansis.red(foo)}!`)); // => 'Hello undefined!' with 'undefined' in red

Why this behaviour is developer-friendly?

  • Explicit Debugging: When undefined or null is displayed, it signals that something unexpected happened, helping developers catch issues.
  • Consistency: Treating all values (even falsy ones) the same ensures predictable behaviour.
  • Avoiding surprises: Silently omitting undefined or null could lead to confusion when debugging logs or user-facing messages.

Libraries like Chalk, Picocolors, and many others follow the "stringify and colorize" approach for undefined values by default. It aligns with developer expectations and makes debugging more intuitive.

@webdiscus
Copy link
Owner

here is the same discussion by colorette

@panoply
Copy link
Author

panoply commented Jan 12, 2025

Hey @webdiscus

Sorry for not being clearer in the issue; I was multitasking when I submitted it (not my strong suit). To circle back and provide more context: while I personally understand the decision to use type casting, and for the majority, this might suffice or never really be an issue, I argue that ANSI modules like ansis, which are designed for string manipulation and highlighting, should adhere to string conventions.

Casting to undefined isn't consistent within the context of string operations. For example, if you call the JavaScript String() constructor without arguments or with null, the result is an empty string, not undefined. Similarly, calling console.log() in Node.js outputs an empty line (a newline character), reinforcing the expectation that even in the absence of input, the result should be an empty string, not an indication of an error or uninitialised state. Moreover, this aligns with broader programming principles where functions or methods dealing with strings default to handling or returning an empty string when no arguments are provided.

Suggestion

If you strongly prefer to retain value casting, I would suggest exposing a global opt-out mechanism. Alternatively, if you agree with my perspective, casting could instead be made opt-in. Since ansis is designed for non-browser runtimes, leveraging a global variable would be both convenient and unobtrusive. For example:

process.env['ANSIS'] = '' // Environment variable to toggle casting

A potentially better solution, however, would be to provide this option directly on the ansis instance itself, as it would allow for casting to be toggled at different points, e.g:

import ansis from 'ansis';

ansis.type_casting = false;

RE "compatibility"

I wanted to get the same result as chalk and picocolors for "compatibility".

I wasn’t aware that casting was used in picocolors/colorette. While I actively avoid using modules written or maintained by Sindre and have no interest in Chalk (so couldn't care less about it), I was genuinely surprised to learn that such behaviour is common among soo many ANSI libraries. My statement about consistency was based on ansi-colors, which defaults to an empty string.

FWIW One of the main reasons I chose ansis is the internal algorithm you’ve developed for highlighting. Personally, I’m fine with less "compatibility" and would much prefer to see ansis become the standard rather than conform to others.

@webdiscus
Copy link
Owner

@panoply

thanks for your answer!

Your arguments are weighty and quite reasonable.
I need to think about it...

P.S. Ansis works w/o options to keep the same result.

@webdiscus
Copy link
Owner

... String() constructor without arguments or with null, the result is an empty string, not undefined. Similarly, calling console.log() ...

But I see a different result:

console.log(String()); // => empty string, OK

console.log(String(null)); // => null
console.log(String(undefined)); // => undefined
console.log(String(NaN)); // => NaN

It looks like this is the expected behavior, at least according to the JS standard.

If you strongly prefer to retain value casting...

I'm for common convention and widely used best practices.

@webdiscus
Copy link
Owner

webdiscus commented Jan 13, 2025

let's first agree on the following values:

  • ansis.green('') - ''
  • ansis.green(1/0) - '\x1b[32mInfinity\x1b[39m'
  • ansis.green(0) - '\x1b[32m0\x1b[39m' <= casting a number to string
  • ansis.green(1) - '\x1b[32m1\x1b[39m' <= casting a number to string
  • ansis.green(true) - '\x1b[32mtrue\x1b[39m' <= casting a boolean to string
  • ansis.green(false) - '\x1b[32mfalse\x1b[39m' <= casting a boolean to string

It's Ok?

@webdiscus
Copy link
Owner

webdiscus commented Jan 13, 2025

What do you think about NaN?

For example:

let foo = 100/'5px'; // NaN, yes it's developer error, but it should not be displayed as an empty string

ansis.green(foo); // => '\x1b[32mNaN\x1b[39m'

I think NaN, like Infinity, should be displayed as its string representation.

@webdiscus
Copy link
Owner

here are results of ansi-colors:

import ansiColors from 'ansi-colors';

console.log(ansiColors.green());          // => empty string
console.log(ansiColors.green(null));      // => empty string
console.log(ansiColors.green(undefined)); // => empty string
console.log(ansiColors.green(''));        // => empty string
console.log(ansiColors.green(true));      // => true
console.log(ansiColors.green(false));     // => false
console.log(ansiColors.green(0));         // => 0
console.log(ansiColors.green(NaN));       // => NaN
console.log(ansiColors.green(1/0));       // => Infinity

I think these are the usualy expected results.

@panoply
Copy link
Author

panoply commented Jan 13, 2025

let's first agree on the following values:

Agreed.

What do you think about NaN?

Yes, NaN is appropriate, same with Infinity

I'd say the conditions that ansi-colors employed is on the mark here, where undefined and null result in empty string whereas all other primitives are cast is the way to go.

  • null > ''
  • undefined > ''

I'd happily settle for that.

@webdiscus webdiscus modified the milestones: analysis, development Jan 13, 2025
webdiscus added a commit that referenced this issue Jan 13, 2025
webdiscus added a commit that referenced this issue Jan 13, 2025
@webdiscus webdiscus modified the milestones: development, test Jan 13, 2025
@webdiscus
Copy link
Owner

@panoply

In v3.9.0, handling of absent, undefined, or null arguments was reverted.
In these cases will be returned an empty string, as before v3.7.0.

See please the handling edge cases in readme.

P.S. Thank you for the constructive discussion!

@webdiscus webdiscus removed the help wanted Extra attention is needed label Jan 13, 2025
@webdiscus webdiscus modified the milestones: test, done Jan 13, 2025
@webdiscus webdiscus removed the question Further information is requested label Jan 13, 2025
@panoply
Copy link
Author

panoply commented Jan 14, 2025

Thanks @webdiscus – Much appreciate the time and discourse.

@panoply panoply closed this as completed Jan 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants