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

fix(types): improve isEmpty signature #219

Merged

Conversation

MarlonPassos-git
Copy link
Contributor

@MarlonPassos-git MarlonPassos-git commented Aug 24, 2024

Tip

The owner of this PR can publish a preview release by commenting /publish in this PR. Afterwards, anyone can try it out by running pnpm add radashi@pr<PR_NUMBER>.

Summary

Improve types with type guards.

Now, anyone using the function will have enhanced type safety.

I don't know if the documentation is needed, But I did this more with people in mind who don't have knowledge of TypeScript's Type Guards.

Related issue, if any:

For any code change,

  • Related documentation has been updated, if needed
  • Related tests have been added or updated, if needed
  • Related benchmarks have been added or updated, if needed

Does this PR introduce a breaking change?

No

@aleclarson aleclarson added this to the v12.3.0 milestone Sep 21, 2024
@aleclarson aleclarson added the better types This PR mainly improves type definitions label Sep 21, 2024
@aleclarson aleclarson changed the title feat(isEmpty): improve types fix(types): improve isEmpty signature Nov 9, 2024
@aleclarson aleclarson removed this from the v12.3.0 milestone Nov 10, 2024
@aleclarson
Copy link
Member

aleclarson commented Nov 10, 2024

I've refactored this PR to avoid multiple overloads, which are an anti-pattern for type guards.

By simplifying to a single call signature, a variable can be any combination of narrowable types. Here's an example of what wasn't possible before my refactor:

const val = {} as string | number | boolean | symbol

if (isEmpty(val)) {
  // val is '' | 0 | false
} else {
  // val is string | number | true | symbol
}

Previously, nothing would get narrowed, because only the (value: unknown): boolean overload would be compatible.

Additionally, the any type can now be narrowed:

declare const val: any

if (isEmpty(val)) {
  // val is false | '' | 0 | readonly never[] | never[] | null | undefined
} else {
  // val is any
}

Object types

Unfortunately, TypeScript doesn't allow us to safely narrow object types in this case. Since TypeScript is a structural type system, we can't guarantee the result of isEmpty based on the type alone, since extra properties may exist at runtime.

Therefore, if the argument is possibly an object type (other than an array or function), we can't safely do any narrowing. Sadly, in TypeScript v5.6.x, a type guard cannot be partially applied.

For example, if we tried to narrow an object type with this:

T extends object
    ? { [K in keyof T]: never }
    : /* ... */

It would be unsafe for a couple reasons:

  1. If an object has no enumerable keys, it may still have a non-enumerable key matching a property defined in the object type. So isEmpty would narrow { a?: string } to { a?: never } even though we can't actually guarantee that the a property is never defined.
  2. Using a check like T extends object is too broad, so a type like Date would pass the condition, resulting in inaccurate types. Nonetheless, we could check for every built-in type using T extends BuiltInType (note that BuiltInType is Radashi-specific).

@aleclarson aleclarson merged commit b95cb73 into radashi-org:main Nov 10, 2024
8 checks passed
@radashi-bot
Copy link

A stable release 12.2.2 has been published to NPM. 🚀

To install:

pnpm add radashi@12.2.2
See the changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
better types This PR mainly improves type definitions
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants