-
Notifications
You must be signed in to change notification settings - Fork 0
feat: enhance isEmpty function #5
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
base: main
Are you sure you want to change the base?
Changes from all commits
0f95045
a34fc62
c85469e
5879c49
4c82f4b
ec2370c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,32 +1,145 @@ | ||
| import js from '@eslint/js'; | ||
| import globals from 'globals'; | ||
| import eslint from '@eslint/js'; | ||
| import tseslint from 'typescript-eslint'; | ||
|
|
||
| export default tseslint.config( | ||
| { | ||
| ignores: ['**/.history/**', 'node_modules/**', 'dist/**'], | ||
| }, | ||
| js.configs.recommended, | ||
| ...tseslint.configs.strictTypeChecked, | ||
| ...tseslint.configs.stylisticTypeChecked, | ||
| eslint.configs.recommended, | ||
| tseslint.configs.strictTypeChecked, | ||
| tseslint.configs.stylisticTypeChecked, | ||
| { | ||
| languageOptions: { | ||
| globals: globals.node, | ||
| parserOptions: { | ||
| projectService: true, | ||
| tsconfigRootDir: import.meta.dirname, | ||
| }, | ||
| }, | ||
| }, | ||
| { | ||
| files: ['**/*.ts'], | ||
| // These rules are defined as errors by "strictTypeChecked" and "stylisticTypeChecked", but do they really cause errors? No. This makes DX worse. | ||
| // Imagine you create a class you plan to finish later with only the constructor. You'll get the "no-extraneous-class" error and won't be able to transpile. | ||
| // If you're like me, you'll get annoyed by that error the whole time and won't be able to focus. | ||
| // So they should be categorized as warnings. | ||
| // The whole idea of this approach is to make sure you can see things that could really break your code and fix them as soon as possible, while letting warnings be warnings. | ||
| rules: { | ||
| '@typescript-eslint/no-inferrable-types': 'off', // Explicit type annotations improve code readability and make intent clearer, especially in utility functions where type safety is paramount. | ||
| '@typescript-eslint/no-unused-vars': 'warn', | ||
| // ================================================================ | ||
| // Error Prevention - Catch bugs and potential runtime errors | ||
| // ================================================================ | ||
| 'no-shadow': 'error', | ||
| 'no-undef': 'error', | ||
| 'no-unreachable': 'warn', | ||
| 'no-var': 'warn', | ||
| 'prefer-const': 'warn', | ||
| '@typescript-eslint/await-thenable': 'warn', | ||
| '@typescript-eslint/no-floating-promises': 'warn', | ||
| '@typescript-eslint/no-for-in-array': 'warn', | ||
| '@typescript-eslint/no-unnecessary-type-assertion': 'warn', | ||
| '@typescript-eslint/no-unused-vars': ['warn'], | ||
| '@typescript-eslint/no-use-before-define': 'warn', | ||
| '@typescript-eslint/require-await': 'warn', | ||
| '@typescript-eslint/strict-boolean-expressions': 'error', | ||
|
|
||
| // ================================================================ | ||
| // Code Quality - Maintainability, readability, best practices | ||
| // ================================================================ | ||
| 'no-alert': 'warn', | ||
| 'no-else-return': 'warn', | ||
| 'no-empty-pattern': 'warn', | ||
| 'no-empty': 'warn', | ||
| 'no-useless-catch': 'warn', | ||
| 'vars-on-top': 'warn', | ||
| 'max-classes-per-file': 'warn', | ||
| 'no-warning-comments': 'warn', // Maintain visibility over FIXMEs and TODOs comments | ||
| '@typescript-eslint/no-extraneous-class': 'warn', | ||
| '@typescript-eslint/no-inferrable-types': 'off', // Explicit type annotations improve readability and make intent clearer, especially in utility functions where type safety is paramount. | ||
| '@typescript-eslint/no-unnecessary-type-parameters': 'warn', | ||
| '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn', | ||
| '@typescript-eslint/no-unnecessary-type-conversion': 'warn', | ||
| '@typescript-eslint/no-unnecessary-template-expression': 'warn', | ||
| '@typescript-eslint/no-empty-function': 'warn', | ||
| '@typescript-eslint/promise-function-async': 'warn', | ||
| '@typescript-eslint/no-empty-object-type': 'warn', | ||
| '@typescript-eslint/no-empty-interface': 'warn', | ||
| '@typescript-eslint/no-explicit-any': 'warn', | ||
| '@typescript-eslint/no-confusing-void-expression': [ | ||
| 'warn', | ||
| { | ||
| ignoreArrowShorthand: true, | ||
| ignoreVoidOperator: false, | ||
| }, | ||
| ], | ||
| '@typescript-eslint/no-unnecessary-condition': 'warn', | ||
| '@typescript-eslint/explicit-function-return-type': 'warn', | ||
| '@typescript-eslint/explicit-module-boundary-types': 'warn', | ||
| '@typescript-eslint/typedef': [ | ||
| 'warn', | ||
| { | ||
| arrayDestructuring: true, | ||
| objectDestructuring: true, | ||
| arrowParameter: true, | ||
| memberVariableDeclaration: true, | ||
| parameter: true, | ||
| propertyDeclaration: true, | ||
| variableDeclaration: true, | ||
| variableDeclarationIgnoreFunction: true, | ||
| }, | ||
| ], | ||
|
|
||
| // ================================================================ | ||
| // Code Style - Formatting and stylistic consistency | ||
| // ================================================================ | ||
| 'padding-line-between-statements': [ | ||
| 'warn', | ||
| { blankLine: 'always', prev: ['const', 'let'], next: '*' }, | ||
| { blankLine: 'any', prev: ['const', 'let'], next: ['const', 'let'] }, | ||
| { blankLine: 'always', prev: '*', next: 'return' }, | ||
| ], | ||
| 'no-continue': 'warn', | ||
| semi: 'warn', | ||
| '@typescript-eslint/consistent-indexed-object-style': 'warn', | ||
| '@typescript-eslint/consistent-type-definitions': 'warn', | ||
| '@typescript-eslint/consistent-type-exports': 'warn', | ||
| '@typescript-eslint/consistent-type-imports': 'warn', | ||
|
|
||
| // ================================================================ | ||
| // Performance - Performance-related rules | ||
| // ================================================================ | ||
| 'prefer-destructuring': 'warn', // Can improve performance by avoiding repeated property access | ||
| 'prefer-template': 'warn', // Template literals are generally faster than string concatenation | ||
| '@typescript-eslint/prefer-nullish-coalescing': 'warn', // More efficient than || for null/undefined checks | ||
| '@typescript-eslint/dot-notation': 'warn', // Bracket notation can be slower than dot notation | ||
|
|
||
| // ================================================================ | ||
| // Security - Security-related rules | ||
| // ================================================================ | ||
| 'no-param-reassign': 'warn', | ||
| 'no-debugger': 'warn', // Debug statements can make the expose of sensitive information easier to see, which is not good in production | ||
| 'no-console': 'warn', // Console statements should not show up in production | ||
| eqeqeq: 'warn', // Prevents type coercion vulnerabilities | ||
| '@typescript-eslint/no-unsafe-call': 'warn', | ||
| '@typescript-eslint/no-unsafe-argument': 'warn', | ||
| '@typescript-eslint/no-unsafe-assignment': 'warn', | ||
| '@typescript-eslint/no-unsafe-member-access': 'warn', | ||
| '@typescript-eslint/no-unsafe-return': 'warn', | ||
| '@typescript-eslint/no-implied-eval': 'warn', // Prevents code injection | ||
| }, | ||
| }, | ||
| { | ||
| // Disable type checking for non-ts files | ||
| // https://typescript-eslint.io/users/configs/#disable-type-checked | ||
| files: ['**/*.mjs'], | ||
| ...tseslint.configs.disableTypeChecked, | ||
| extends: [tseslint.configs.disableTypeChecked], | ||
| rules: { | ||
| 'no-console': 'off', | ||
| }, | ||
| }, | ||
| { | ||
| files: ['**/*.test.ts'], | ||
| extends: [tseslint.configs.disableTypeChecked], | ||
| rules: { | ||
| // We don't need to be that strict with tests | ||
| '@typescript-eslint/typedef': 'off', | ||
| }, | ||
| }, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,47 +1,102 @@ | ||
| import { getTag } from './_getTag'; | ||
| import { isPlainObject } from './isPlainObject'; | ||
|
|
||
| /** | ||
| * Checks if the given value is empty. | ||
| * | ||
| * 🚨 The values listed below are not considered empty (they are falsy, not empty): | ||
| * ## What is considered empty: | ||
| * - `null` and `undefined` | ||
| * - Empty strings (`""`) and whitespace-only strings (`" "`) | ||
| * - `NaN` (Not a Number) | ||
| * - Empty arrays (`[]`) | ||
| * - Empty Map and Set objects | ||
| * - Objects with no enumerable properties (`{}`) | ||
| * | ||
| * ## Design Philosophy: Empty vs Falsy | ||
| * | ||
| * This function is specifically designed to check for **emptiness**, not **falsiness**. | ||
| * While JavaScript's falsy values (`false`, `0`, `""`, `null`, `undefined`, `NaN`, `0n`) | ||
| * all evaluate to `false` in boolean contexts, they serve different semantic purposes: | ||
| * | ||
| * - **Falsy values** represent "false-like" states in boolean logic | ||
| * - **Empty values** represent "absence of content" or "no data" | ||
| * | ||
| * ### Why the distinction matters: | ||
| * | ||
| * 1. **`false`** is a valid boolean state, not empty data | ||
| * 2. **`0`** is a valid number representing zero, not absence of a number | ||
| * 3. **`0n`** is a valid BigInt representing zero, not absence of a BigInt | ||
| * 4. **`""`** represents no textual content - this IS empty | ||
| * 5. **`null`/`undefined`** represent absence of value - these ARE empty | ||
| * 6. **`NaN`** represents an invalid/failed computation - this IS empty | ||
| * | ||
| * ### Use cases: | ||
| * - **Form validation**: `isEmpty("")` → true, `isEmpty(false)` → false | ||
| * - **Data processing**: `isEmpty([])` → true, `isEmpty(0)` → false | ||
| * - **API responses**: `isEmpty({})` → true, `isEmpty({count: 0})` → false | ||
| * | ||
| * ## What is NOT considered empty (valid data, even if falsy): | ||
| * - The boolean `false` | ||
| * - The number zero `0` | ||
| * - The number zero `0`, `-0`, `Infinity`, `-Infinity` | ||
| * - The BigInt zero `BigInt(0)` or `0n` | ||
| * - Symbols (all symbols are considered valid data) | ||
| * | ||
| * ## Special considerations: | ||
| * - Objects are considered empty if they have no enumerable properties | ||
| * - Objects with only symbol-keyed or non-enumerable properties are considered empty | ||
| * - WeakMap and WeakSet are not supported because there is no way to check if they are empty and this is a limitation created by design of these objects | ||
| * | ||
| * @param value - The value to check. | ||
| * @returns `true` if the value is empty, `false` otherwise. | ||
| */ | ||
| export function isEmpty(value: unknown): boolean { | ||
| if (typeof value === 'boolean') { | ||
| return false; | ||
| } | ||
|
|
||
| if (value === undefined || value === null) { | ||
| return true; | ||
| } | ||
|
|
||
| if (typeof value === 'string') { | ||
| if (typeof value === 'boolean' || value instanceof Boolean) { | ||
| return false; | ||
| } | ||
|
|
||
| if (typeof value === 'string' || value instanceof String) { | ||
| return value.trim() === ''; | ||
| } | ||
|
|
||
| if (typeof value === 'number') { | ||
| /** | ||
| * `isNaN` checks whether the value is not a number or cannot be converted | ||
| * into a number. `Number.isNaN` only checks if the value is equal to NaN. | ||
| */ | ||
| return isNaN(value); | ||
| if (typeof value === 'number' || value instanceof Number) { | ||
| // Only NaN is treated as empty; all other numbers, | ||
| // including 0, -0, ±Infinity are not empty. | ||
| return Number.isNaN(value.valueOf()); | ||
| } | ||
|
|
||
| if (Array.isArray(value)) { | ||
| return value.length === 0; | ||
| if (typeof value === 'bigint' || value instanceof BigInt) { | ||
| return false; | ||
| } | ||
|
|
||
| if (value instanceof Map || value instanceof Set) { | ||
| return value.size === 0; | ||
| } | ||
|
|
||
| if (typeof value === 'object') { | ||
| if (Array.isArray(value)) { | ||
| return value.length === 0; | ||
| } | ||
|
|
||
| const tag = getTag(value); | ||
|
Check warning on line 83 in src/isEmpty.ts
|
||
|
|
||
| if (/^\[object (?:[A-Z]\w*Array)\]$/.test(tag)) { | ||
|
Check warning on line 85 in src/isEmpty.ts
|
||
| return (value as ArrayLike<unknown>).length === 0; | ||
| } | ||
|
|
||
| if (isPlainObject(value)) { | ||
| return Object.keys(value).length === 0; | ||
| } | ||
|
|
||
| throw new Error( | ||
| `The given argument could not be parsed: ${JSON.stringify(value)}`, | ||
| ); | ||
| if (typeof value === 'symbol' || value instanceof Symbol) { | ||
| return false; | ||
| } | ||
|
|
||
| if (typeof value === 'function') { | ||
| return false; | ||
| } | ||
|
|
||
| throw new TypeError(`The given argument is not supported: ${tag}`); | ||
|
Check failure on line 101 in src/isEmpty.ts
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.