remeda
Version:
A utility library for JavaScript and Typescript.
1 lines • 10 kB
Source Map (JSON)
{"version":3,"file":"isEmptyish.cjs","names":[],"sources":["../src/isEmptyish.ts"],"sourcesContent":["import type {\n And,\n HasRequiredKeys,\n IsAny,\n IsEqual,\n IsNever,\n IsNumericLiteral,\n IsUnknown,\n OmitIndexSignature,\n Or,\n Tagged,\n ValueOf,\n} from \"type-fest\";\nimport type { HasWritableKeys } from \"./internal/types/HasWritableKeys\";\nimport type { TupleParts } from \"./internal/types/TupleParts\";\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars -- we use a non-exported unique symbol to prevent users from faking our return type.\ndeclare const EMPTYISH_BRAND: unique symbol;\n\n// Because our function is a type-predicate and it narrows the input based on\n// the result of our type, we sometimes need a way to \"turn off\" narrowing while\n// still returning the input type. By tagging/branding our return type we stop\n// TypeScript from narrowing it while still allowing it to be used as if it was\n// the input type (because it still extends the type).\ntype Empty<T> = Tagged<T, typeof EMPTYISH_BRAND>;\n\n// The goal of this type is to return the empty \"view\" of the input type. This\n// makes it possible for TypeScript to narrow it precisely.\ntype Emptyish<T> =\n // There are effectively 4 types that can be empty:\n | (T extends string ? \"\" : never)\n | (T extends object ? EmptyishObjectLike<T> : never)\n | (T extends null ? null : never)\n | (T extends undefined ? undefined : never);\n\n// Because of TypeScript's duck-typing, a lot of sub-types of `object` can\n// extend each other so we need to cascade between the different \"kinds\" of\n// objects.\ntype EmptyishObjectLike<T extends object> =\n T extends ReadonlyArray<unknown>\n ? EmptyishArray<T>\n : T extends ReadonlyMap<infer Key, unknown>\n ? T extends Map<unknown, unknown>\n ? // Mutable maps should remain mutable so we can't narrow them down.\n Empty<T>\n : // But immutable maps could be rewritten to prevent any mutations.\n ReadonlyMap<Key, never>\n : T extends ReadonlySet<unknown>\n ? T extends Set<unknown>\n ? // Mutable sets should remain mutable so we can't narrow them down.\n Empty<T>\n : // But immutable sets could be rewritten to prevent any mutations.\n ReadonlySet<never>\n : EmptyishObject<T>;\n\ntype EmptyishArray<T extends ReadonlyArray<unknown>> = T extends readonly []\n ? // By returning T we effectively narrow the \"else\" branch to `never`.\n T\n : And<\n IsEqual<TupleParts<T>[\"required\"], []>,\n IsEqual<TupleParts<T>[\"suffix\"], []>\n > extends true\n ? T extends Array<unknown>\n ? // A mutable array should remain mutable so we can't narrow it down.\n Empty<T>\n : // But immutable arrays could be rewritten to prevent any mutations.\n readonly []\n : // An array with a required prefix or suffix would never be empty, we can\n // use that fact to narrow the \"if\" branch to `never`.\n never;\n\ntype EmptyishObject<T extends object> = T extends {\n length: infer Length extends number;\n}\n ? T extends string\n ? // When a string is tagged/branded it also extends `object` and also has\n // a `length` prop so we need to prevent handling it because it's\n // irrelevant here!\n never\n : // Because of how the implementation works, we need to consider any object\n // with a `length` prop as potentially \"empty\".\n EmptyishArbitrary<T, Length>\n : T extends { size: infer Size extends number }\n ? // Because of how the implementation works, we need to consider any object\n // with a `size` prop as potentially \"empty\".\n EmptyishArbitrary<T, Size>\n : IsNever<ValueOf<T>> extends true\n ? // This handles empty objects; by returning T we effectively narrow the\n // \"else\" branch to `never`.\n T\n : HasRequiredKeys<OmitIndexSignature<T>> extends true\n ? // If the object has required keys it can never be empty, we can use\n // that fact to narrow the \"if\" branch to `never`.\n never\n : HasWritableKeys<T> extends true\n ? // A mutable object should remain mutable so we can't narrow it\n // down.\n Empty<T>\n : // But immutable objects could be rewritten to prevent any\n // mutations.\n { readonly [P in keyof T]: never };\n\n// We use certain props to check for emptiness effectively, but that means we\n// will return those values for any object that has them. Because we don't know\n// anything about those objects we need to be careful about narrowing.\ntype EmptyishArbitrary<T, N> =\n IsNumericLiteral<N> extends true\n ? [0] extends [N]\n ? [N] extends [0]\n ? // If the prop is a literal 0 the object is and always will be empty\n // so we can return it to narrow the \"else\" branch as `never`.\n T\n : // If it accepts 0, but might accept other values too we need to\n // consider the object mutable and not narrow it down.\n Empty<T>\n : // If the prop will never be 0 we can say it will never be empty and can\n // return `never` for the \"if\" branch.\n never\n : // If the prop isn't a literal value we don't know enough about the object\n // and should consider it mutable.\n Empty<T>;\n\n// Overly generic types interfere with our already pretty complex return type.\n// To make our lives easier we can filter them out at the function declaration\n// step and we never need to think about them again.\ntype ShouldNotNarrow<T> = Or<\n Or<IsAny<T>, IsUnknown<T>>,\n IsEqual<\n T,\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n {}\n >\n>;\n\n/**\n * A function that checks if the input is empty. Empty is defined as anything\n * exposing a numerical `length`, or `size` property that is equal to `0`. This\n * definition covers strings, arrays, Maps, Sets, plain objects, and custom\n * classes. Additionally, `null` and `undefined` are also considered empty.\n *\n * `number`, `bigint`, `boolean`, `symbol`, and `function` will always return\n * `false`. `RegExp`, `Date`, and weak collections will always return `true`.\n * Classes and Errors are treated as plain objects: if they expose any public\n * property they would be considered non-empty, unless they expose a numerical\n * `length` or `size` property, which defines their emptiness regardless of\n * other properties.\n *\n * This function has *limited* utility at the type level because **negating** it\n * does not yield a useful type in most cases because of TypeScript\n * limitations. Additionally, utilities which accept a narrower input type\n * provide better type-safety on their inputs. In most cases, you should use\n * one of the following functions instead:\n * * `isEmpty` - provides better type-safety on inputs by accepting a narrower set of cases.\n * * `hasAtLeast` - when the input is just an array/tuple.\n * * `isStrictEqual` - when you just need to check for a specific literal value.\n * * `isNullish` - when you just care about `null` and `undefined`.\n * * `isTruthy` - when you need to also filter `number` and `boolean`.\n *\n * @param data - The variable to check.\n * @signature\n * R.isEmptyish(data)\n * @example\n * R.isEmptyish(undefined); //=> true\n * R.isEmptyish(null); //=> true\n * R.isEmptyish(''); //=> true\n * R.isEmptyish([]); //=> true\n * R.isEmptyish({}); //=> true\n * R.isEmptyish(new Map()); //=> true\n * R.isEmptyish(new Set()); //=> true\n * R.isEmptyish({ a: \"hello\", size: 0 }); //=> true\n * R.isEmptyish(/abc/); //=> true\n * R.isEmptyish(new Date()); //=> true\n * R.isEmptyish(new WeakMap()); //=> true\n *\n * R.isEmptyish('test'); //=> false\n * R.isEmptyish([1, 2, 3]); //=> false\n * R.isEmptyish({ a: \"hello\" }); //=> false\n * R.isEmptyish({ length: 1 }); //=> false\n * R.isEmptyish(0); //=> false\n * R.isEmptyish(true); //=> false\n * R.isEmptyish(() => {}); //=> false\n * @category Guard\n */\nexport function isEmptyish<T>(\n data: ShouldNotNarrow<T> extends true\n ? never\n : T | Readonly<Emptyish<NoInfer<T>>>,\n): data is ShouldNotNarrow<T> extends true\n ? never\n : T extends unknown\n ? Emptyish<NoInfer<T>>\n : never;\nexport function isEmptyish(data: unknown): boolean;\n\nexport function isEmptyish(data: unknown): boolean {\n // eslint-disable-next-line eqeqeq -- Less code to ship...\n if (data == undefined || data === \"\") {\n // These are the only literal values that are considered emptyish.\n return true;\n }\n\n if (typeof data !== \"object\") {\n // There are no non-object types that could be empty at this point...\n return false;\n }\n\n if (\"length\" in data && typeof data.length === \"number\") {\n // Arrays and array-likes.\n return data.length === 0;\n }\n\n if (\"size\" in data && typeof data.size === \"number\") {\n // Maps and Sets.\n return data.size === 0;\n }\n\n // eslint-disable-next-line guard-for-in, no-unreachable-loop -- Instead of taking Object.keys just to check its length, which will be inefficient if the object has a lot of keys, we have a backdoor into an iterator of the object's properties via the `for...in` loop.\n for (const _ in data) {\n return false;\n }\n\n // We can't do a similar optimization for symbol props, so we leave them for\n // the very last check when the object is practically empty. Assuming that\n // even if an object has a symbol prop, it probably doesn't have thousands of\n // them.\n return Object.getOwnPropertySymbols(data).length === 0;\n}\n"],"mappings":"AAkMA,SAAgB,EAAW,EAAwB,CAEjD,GAAI,GAAQ,MAAa,IAAS,GAEhC,MAAO,GAGT,GAAI,OAAO,GAAS,SAElB,MAAO,GAGT,GAAI,WAAY,GAAQ,OAAO,EAAK,QAAW,SAE7C,OAAO,EAAK,SAAW,EAGzB,GAAI,SAAU,GAAQ,OAAO,EAAK,MAAS,SAEzC,OAAO,EAAK,OAAS,EAIvB,IAAK,IAAM,KAAK,EACd,MAAO,GAOT,OAAO,OAAO,sBAAsB,EAAK,CAAC,SAAW"}