Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 123 additions & 10 deletions eslint.config.mjs
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',
},
},
);
13 changes: 5 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@
"version": "1.0.0-alpha.1",
"description": "Generic utility functions for JavaScript",
"license": "ISC",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
Expand All @@ -17,9 +14,9 @@
"scripts": {
"build": "rollup -c",
"build:watch": "rollup -c -w",
"check-types": "tsc --pretty --noEmit --project tsconfig.json",
"lint": "eslint '{src,tests}/**/*.ts'",
"lint:fix": "eslint '{src,tests}/**/*.ts' --fix",
"check-types": "tsc --pretty --project tsconfig.json",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"test": "jest",
"test:watch": "jest --watch",
Expand Down Expand Up @@ -55,7 +52,7 @@
"dist"
],
"engines": {
"node": "22.x",
"node": ">=18",
"pnpm": "10.x"
},
"packageManager": "pnpm@10.15.0"
Expand Down
15 changes: 2 additions & 13 deletions rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import commonjs from '@rollup/plugin-commonjs';
import dts from 'rollup-plugin-dts';

const config = [

Check warning on line 6 in rollup.config.mjs

View workflow job for this annotation

GitHub Actions / test (18)

Expected config to have a type annotation

Check warning on line 6 in rollup.config.mjs

View workflow job for this annotation

GitHub Actions / test (22)

Expected config to have a type annotation

Check warning on line 6 in rollup.config.mjs

View workflow job for this annotation

GitHub Actions / test (18)

Expected config to have a type annotation

Check warning on line 6 in rollup.config.mjs

View workflow job for this annotation

GitHub Actions / test (23)

Expected config to have a type annotation

Check warning on line 6 in rollup.config.mjs

View workflow job for this annotation

GitHub Actions / test (20)

Expected config to have a type annotation

Check warning on line 6 in rollup.config.mjs

View workflow job for this annotation

GitHub Actions / test (20)

Expected config to have a type annotation

Check warning on line 6 in rollup.config.mjs

View workflow job for this annotation

GitHub Actions / test (24)

Expected config to have a type annotation

Check warning on line 6 in rollup.config.mjs

View workflow job for this annotation

GitHub Actions / test (22)

Expected config to have a type annotation

Check warning on line 6 in rollup.config.mjs

View workflow job for this annotation

GitHub Actions / test (19)

Expected config to have a type annotation

Check warning on line 6 in rollup.config.mjs

View workflow job for this annotation

GitHub Actions / test (23)

Expected config to have a type annotation
{
input: 'src/index.ts',
output: [
{
file: 'dist/index.js',
file: 'dist/index.mjs',
format: 'es',
sourcemap: true,
},
Expand All @@ -22,10 +22,6 @@
nodeResolve(),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
declaration: true,
declarationDir: './dist',
outDir: './dist',
exclude: ['tests/**/*'],
}),
],
Expand All @@ -34,14 +30,7 @@
{
input: 'src/index.ts',
output: [{ file: 'dist/index.d.ts', format: 'es' }],
plugins: [
dts({
compilerOptions: {
baseUrl: '.',
paths: { '@/*': ['./src/*'] },
},
}),
],
plugins: [dts()],
},
];

Expand Down
93 changes: 74 additions & 19 deletions src/isEmpty.ts
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

View workflow job for this annotation

GitHub Actions / test (18)

Unsafe call of a(n) `error` type typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (18)

Unsafe assignment of an error typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (18)

Expected tag to have a type annotation

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (22)

Unsafe call of a(n) `error` type typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (22)

Unsafe assignment of an error typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (22)

Expected tag to have a type annotation

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (18)

Unsafe call of a(n) `error` type typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (18)

Unsafe assignment of an error typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (18)

Expected tag to have a type annotation

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (23)

Unsafe call of a(n) `error` type typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (23)

Unsafe assignment of an error typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (23)

Expected tag to have a type annotation

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (20)

Unsafe call of a(n) `error` type typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (20)

Unsafe assignment of an error typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (20)

Expected tag to have a type annotation

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (20)

Unsafe call of a(n) `error` type typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (20)

Unsafe assignment of an error typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (20)

Expected tag to have a type annotation

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (24)

Unsafe call of a(n) `error` type typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (24)

Unsafe assignment of an error typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (24)

Expected tag to have a type annotation

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (22)

Unsafe call of a(n) `error` type typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (22)

Unsafe assignment of an error typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (22)

Expected tag to have a type annotation

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (19)

Unsafe call of a(n) `error` type typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (19)

Unsafe assignment of an error typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (19)

Expected tag to have a type annotation

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (23)

Unsafe call of a(n) `error` type typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (23)

Unsafe assignment of an error typed value

Check warning on line 83 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (23)

Expected tag to have a type annotation

if (/^\[object (?:[A-Z]\w*Array)\]$/.test(tag)) {

Check warning on line 85 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (18)

Unsafe argument of type error typed assigned to a parameter of type `string`

Check warning on line 85 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (22)

Unsafe argument of type error typed assigned to a parameter of type `string`

Check warning on line 85 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (18)

Unsafe argument of type error typed assigned to a parameter of type `string`

Check warning on line 85 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (23)

Unsafe argument of type error typed assigned to a parameter of type `string`

Check warning on line 85 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (20)

Unsafe argument of type error typed assigned to a parameter of type `string`

Check warning on line 85 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (20)

Unsafe argument of type error typed assigned to a parameter of type `string`

Check warning on line 85 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (24)

Unsafe argument of type error typed assigned to a parameter of type `string`

Check warning on line 85 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (22)

Unsafe argument of type error typed assigned to a parameter of type `string`

Check warning on line 85 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (19)

Unsafe argument of type error typed assigned to a parameter of type `string`

Check warning on line 85 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (23)

Unsafe argument of type error typed assigned to a parameter of type `string`
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

View workflow job for this annotation

GitHub Actions / test (18)

Invalid type "any" of template literal expression

Check failure on line 101 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (22)

Invalid type "any" of template literal expression

Check failure on line 101 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (18)

Invalid type "any" of template literal expression

Check failure on line 101 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (23)

Invalid type "any" of template literal expression

Check failure on line 101 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (20)

Invalid type "any" of template literal expression

Check failure on line 101 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (20)

Invalid type "any" of template literal expression

Check failure on line 101 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (24)

Invalid type "any" of template literal expression

Check failure on line 101 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (22)

Invalid type "any" of template literal expression

Check failure on line 101 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (19)

Invalid type "any" of template literal expression

Check failure on line 101 in src/isEmpty.ts

View workflow job for this annotation

GitHub Actions / test (23)

Invalid type "any" of template literal expression
}
3 changes: 2 additions & 1 deletion src/isPlainObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function isPlainObject(
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
Object.getPrototypeOf(value) === Object.prototype
(Object.getPrototypeOf(value) === Object.prototype ||
Object.getPrototypeOf(value) === null)
);
}
Loading
Loading