sindresorhus / is

Type check values
MIT License
1.68k stars 109 forks source link

Typing errors with the isEmptyStringOrWhitespace type guard #206

Closed vtgn closed 4 months ago

vtgn commented 5 months ago

Hi!

There is a problem similar to the issue #176 with the isEmptyStringOrWhitespace type guard:

image

Indeed, this type guard is incorrect because the type is not enough precise and it leads to typing problems when it returns false. Example:

image

The workaround is to use a type assertion with the as keyword for the cases where the compiler see values as non string whereas they are. :/

I don't know if it is possible to create a precise type for empty or whitespaces strings. I will try and tell you if I find.

Regards.

marlun78 commented 5 months ago

One possibility would be to redefine isWhitespaceString and isEmptyStringOrWhitespace as follows:

function isWhitespaceString(value: unknown): value is " " {
  return isString(value) && /^\s+$/.test(value);
}

function isEmptyStringOrWhitespace(value: unknown): value is "" | " " {
  return isEmptyString(value) || isWhitespaceString(value);
}

The behavior would be more accurate, but still not 100% correct as a string could contain any number of whitespaces. However, I don't think that it's possible to express that in TS. Also, it would be it would be a breaking change.

See example in the playground.

sindresorhus commented 5 months ago

We can actually type it correctly: https://github.com/sindresorhus/type-fest/blob/0f732371f607fe44e934d178eb97ad71eccda873/source/internal.d.ts#L315-L322 But I don't think that would be very useful.

I think the solution above is the most practical one.

vtgn commented 5 months ago

@marlun78 it is not perfect indeed, but it is a better solution than the current situation.

@sindresorhus there is another solution, more appropriate in my opinion, which consists of using the "opaque types" concept. You must create a simple type that "simulates" the complex type you are interested in, and create a type guard function using it. You can see what the developer of the zod library did with the brand feature: https://github.com/colinhacks/zod#brand You define in your library a unique symbol and a parameterized type that you use to create any "simulated" complex type. Zod does it like this:

export const BRAND: unique symbol = Symbol("zod_brand");
export type BRAND<T extends string | number | symbol> = {
  [BRAND]: { [k in T]: true };
};

Then you create the type for the empty or whitespaces (\t\n\t\r\v) strings, by intersecting the global type (string here) with the BRAND type adding the virtual definition of the subset of the global type (here it means the empty or whitespaces string subset):

export type EmptyOrWhitespacesString = string & BRAND<"EmptyOrWhitespacesString">;

Then you modify the declaration of your isEmptyStringOrWhitespace type guard function (same for your assertEmptyStringOrWhitespace assertion guard function) to use this type:

function isEmptyStringOrWhitespace(value: unknown): value is EmptyOrWhitespacesString

Thus, you have a type matching perfectly the empty or whitespaces strings, even if it is "virtually", but it is enough to manage the typing correctly in every cases. Example of use:

let value: ATYPE = <any value>; // ATYPE can be any type (string, unknown, Object, any, number | boolean | null, etc...)

if (is.emptyStringOrWhitespace(value)) {
   value; // => is typed by EmptyOrWhitespacesString and allows to use value as a string because this is the global type we use to define the EmptyOrWhitespacesString type (value.length is allowed for example)
}
else {
   value; // => is typed by ATYPE
}

value; // => is typed by ATYPE

and this is exactly the behavior we wanted. ;) If you replace ATYPE by string, and you store a non empty and non whitespaces string value in the value variable, the type guard returns false, but you can nevertheless continue to use value as string in the else block, what is not possible with the current implementation of the type guard.

Regards.

vtgn commented 4 months ago

=/ ...

sindresorhus commented 4 months ago

@vtgn Sorry, I missed your last comment here. Are there any benefits of the "brand" over what's in https://github.com/sindresorhus/is/commit/25a376875d3c10e3963e2e948960162a221fe583? The benefit of '' | Whitespace is that it has more info. For example, the user could potentially statically check if the string is empty.

One hybrid approach would be to use:

export type EmptyOrWhitespacesString = '' | (string & BRAND<"WhitespacesString">);

export function isEmptyStringOrWhitespace(value: unknown): value is EmptyOrWhitespacesString {