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

Typing errors with the isEmptyStringOrWhitespace type guard #206

Open
vtgn opened this issue May 21, 2024 · 3 comments · May be fixed by #207
Open

Typing errors with the isEmptyStringOrWhitespace type guard #206

vtgn opened this issue May 21, 2024 · 3 comments · May be fixed by #207

Comments

@vtgn
Copy link

vtgn commented May 21, 2024

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
Copy link
Contributor

marlun78 commented May 22, 2024

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
Copy link
Owner

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
Copy link
Author

vtgn commented May 22, 2024

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants