@mongez/supportive-is
v3.0.0
Published
Tree-shakable type & shape predicates: isString, isEmpty, isUrl, isJson, isEmail, isPromise, isMobile, … framework-agnostic and dependency-free.
Maintainers
Readme
@mongez/supportive-is
Tree-shakable type and shape predicates for JavaScript — isString, isEmpty, isUrl, isPromise, isMobile.any() — one named export each, zero runtime dependencies.
Why @mongez/supportive-is?
Raw typeof and instanceof only get you so far — typeof null is "object", typeof [] is "object", and there's no built-in answer to "is this thing empty in the way I mean?". lodash.isString (and friends) work, but you either pay for the full lodash build or wire up a separate package per check and hope tree-shaking strips what you don't use. validator.js does email, URL, and friends, but it's a heavyweight string-validation library with a different scope.
@mongez/supportive-is is the smallest middle ground: one named export per predicate, sideEffects: false so bundlers actually drop the unused ones, zero runtime dependencies, and a Date/Map/Set/plain-object-aware isEmpty that collapses the ten lines you'd write by hand into one call. The package is shape predicates only — it tells you what kind of value you have, not how to transform it. Transformations live in @mongez/reinforcements.
import { isEmail, isEmpty, isPlainObject, isUrl } from "@mongez/supportive-is";
isEmail("[email protected]"); // true
isEmpty({}); // true
isEmpty(0); // false — zero is a real value
isPlainObject(new Date()); // false — Date is an instance, not a literal
isUrl("https://example.com"); // trueFeatures
| Feature | Description |
|---|---|
| Tree-shakable named exports | import { isString } brings in ~80 bytes; the unused predicates fall away. sideEffects: false is set. |
| Primitive checks | isString, isNumeric, isInt, isFloat, isPrimitive, isScalar — covers numeric-strings, signed numbers, and the bigint/symbol split. |
| Collection / shape checks | isObject, isPlainObject, isIterable, isEmpty plus Is.array — distinguishes {} from class instances, handles Map/Set/Date correctly. |
| Format predicates | isJson, isUrl, isEmail, isRegex, isValidId — convenience filters for the four formats every project needs. |
| Environment detection | isMobile.android()/.ios()/.iphone()/.ipad()/.ipod()/.windows()/.any(), isDesktop, isMac, isBrowser(name), plus vendor probes (isChrome/isFirefox/isSafari/isOpera/isIE/isEdge). |
| Object-kind checks | isPromise, isDate, isGenerator, isFormElement, isFormData. |
| Smart isEmpty | One call covers null, undefined, "", [], {}, new Map(), new Set() — with 0, false, new Date(), and NaN deliberately reported as not empty. |
| Legacy Is namespace | import Is from "@mongez/supportive-is"; Is.empty(x) still works — same functions, different shape, no tree-shaking. |
| SSR-safe imports | DOM-touching predicates read navigator/window/document at call time, never at module-eval. Importing the package on the server is always safe. |
| Zero runtime dependencies | One source file. No transitive packages. |
| TypeScript-first | Twelve checks are real type predicates on unknown (isString, isPlainObject, isDate, isPromise, isIterable, ...) so tsc narrows for you. See Type predicates. |
Installation
npm install @mongez/supportive-isyarn add @mongez/supportive-ispnpm add @mongez/supportive-isQuick start
import {
isString,
isEmpty,
isNumeric,
isUrl,
isEmail,
isPlainObject,
isPromise,
isMobile,
} from "@mongez/supportive-is";
isString("hello"); // true
isEmpty({}); // true
isEmpty(0); // false — zero is not empty
isNumeric("12.5"); // true
isUrl("https://example.com"); // true
isEmail("[email protected]"); // true
isPlainObject(new Date()); // false
isPromise(fetch("/api")); // true
if (isMobile.any()) { /* mobile UI */ }That's the entire happy path. Everything below is depth on the same set of predicates.
Primitive checks
Six predicates for primitive and numeric types. All run in O(1) everywhere.
| Export | Rule | Notes |
|---|---|---|
| isString(value) | typeof value === "string" | TS narrows correctly. Excludes new String("x"). |
| isNumeric(value) | Number or numeric string | Accepts "12", "-1.5", "+1", "1.5E-3". Rejects "", "1.", "12abc". |
| isInt(value) | typeof value === "number" AND Number.isInteger(value) | Accepts signed ints, rejects 1.5, "2", NaN, Infinity. |
| isFloat(value) | Finite number whose string form has a fractional part | Accepts 1.5, -0.1. Rejects 1, 1.0, "1.5". |
| isPrimitive(value) | string / number / boolean / bigint | Excludes symbol and null/undefined. |
| isScalar(value) | string / number / boolean / bigint / symbol | The "anything that isn't an object reference" check. |
import { isNumeric, isInt, isFloat, isPrimitive, isScalar } from "@mongez/supportive-is";
isNumeric(0); // true
isNumeric("12"); // true
isNumeric("1e10"); // true
isNumeric("1."); // false — trailing dot
isNumeric("hello"); // false
isInt(0); // true
isInt(-1); // true
isInt(1.5); // false
isInt("2"); // false — data type matters
isFloat(1.5); // true
isFloat(-0.1); // true
isFloat(1); // false — no fractional part
isFloat(1.0); // false — `String(1.0) === "1"`
isPrimitive(Symbol("x")); // false — by design
isScalar(Symbol("x")); // trueTypeScript:
isStringnarrows tostring, and the other type-checking guards (isPlainObject,isDate,isPromise, ...) are real type predicates too. The refinement checks (isInt,isFloat,isNumeric,isEmpty, ...) stayboolean. See Type predicates.
Collection and shape checks
Five predicates that answer "what shape is this, and is there anything in it?".
| Export | Rule |
|---|---|
| isObject(value) | Truthy AND typeof value === "object". Includes arrays, dates, regexes, class instances. Excludes null and functions. |
| isPlainObject(value) | {} / new Object() / Object.create(null) only — not arrays, dates, class instances. |
| Is.array(value) | Alias for Array.isArray. |
| isIterable(value) | Implements [Symbol.iterator] — arrays, strings, Set, Map, custom generators. |
| isEmpty(value) | Smart emptiness — see Empty semantics. |
import { isObject, isPlainObject, isIterable, isEmpty, Is } from "@mongez/supportive-is";
isObject({}); // true
isObject([]); // true
isObject(new Date()); // true
isObject(null); // false
isPlainObject({ a: 1 }); // true
isPlainObject(Object.create(null)); // true
isPlainObject([]); // false
isPlainObject(new Date()); // false
isPlainObject(new class {}); // false
Is.array([1, 2, 3]); // true
Is.array(new Set()); // false — iterable, but not an Array
Is.array({ length: 0 }); // false — array-like, but not an Array
isIterable("hello"); // true
isIterable(""); // true — empty string IS iterable
isIterable(new Map()); // true
isIterable({}); // falseisPlainObject is the right predicate for "should I merge into this, or replace it?" — class instances, Date, Map, Set, and RegExp all want to be replaced wholesale, and isPlainObject returns false for every one of them. See the deep-merge recipe.
Empty semantics
isEmpty is the most-used predicate in the package. It collapses the ten lines you'd write by hand into one:
| Input | Result | Why |
|---|---|---|
| null, undefined, "" | true | Listed in the empty set |
| [], {} | true | Zero own keys / zero length |
| new Map(), new Set() | true | .size === 0 |
| 0, true, false, "0", " " | false | Real values |
| [0], { a: 1 }, 1, -1 | false | Has content |
| new Date() | false | A constructed date is not empty |
| NaN | false | Still a numeric value |
| Other iterables | .length === 0 | Generic check |
import { isEmpty } from "@mongez/supportive-is";
isEmpty(null); // true
isEmpty(undefined); // true
isEmpty(""); // true
isEmpty([]); // true
isEmpty({}); // true
isEmpty(new Map()); // true
isEmpty(new Set()); // true
isEmpty(0); // false — zero is a real value
isEmpty(false); // false — false is a real value
isEmpty("0"); // false — string "0" is not empty
isEmpty(" "); // false — whitespace counts
isEmpty([0]); // false — one element
isEmpty({ a: 1 }); // false — one key
isEmpty(new Date()); // false — constructed Date is not empty
isEmpty(NaN); // false — still a numeric value
isEmptydoes not trim whitespace.isEmpty(" ")returnsfalse. If you mean "empty after trimming", callisEmpty(value?.trim?.() ?? value)at the call site, or use a dedicated string library.
Falsy-return note
isObject, isPlainObject, isRegex, isPromise, isDate, and isIterable use the truthiness shape Boolean(value) && … — they return real false for null/undefined. In if/!/ternary contexts they behave exactly as you'd expect. Don't compare them to false with ===:
import { isObject } from "@mongez/supportive-is";
if (isObject(null) === false) { /* runs */ } // safe — returns real false
if (!isObject(null)) { /* runs */ } // idiomaticFormat predicates
Five string-format predicates. None of these are security gates. They're convenience filters — for input you don't trust (auth flows, redirect targets, persisted emails), pair them with a real validator like zod or valibot, or do the actual operation in a try/catch.
| Export | Rule |
|---|---|
| isRegex(value) | RegExp instance or regex literal. |
| isValidId(value) | Valid HTML id attribute — /^[A-Za-z]+[\w\-:.]*$/. Also aliased as Is.validHtmlId. |
| isJson(value) | Valid JSON string starting with { or [. Rejects primitive JSON ("true", "123", '"hello"'). |
| isUrl(value) | Constructable new URL(value) with http: / https: protocol and a non-empty dotted hostname. |
| isEmail(value) | Standard RFC 5322 subset regex. |
import { isRegex, isValidId, isJson, isUrl, isEmail } from "@mongez/supportive-is";
isRegex(/x/); // true
isRegex(new RegExp("x")); // true
isRegex("/x/"); // false — string, not RegExp
isValidId("base-id"); // true
isValidId("has.dots"); // true
isValidId("has:colon"); // true
isValidId("1starts-with-digit"); // false
isValidId("_starts-with-underscore"); // false
isValidId(null); // false — real boolean
isJson('{"name":"John"}'); // true
isJson("[1, 2, 3]"); // true
isJson("12"); // false — numeric JSON rejected by prefix
isJson('"hello"'); // false — string JSON rejected by prefix
isJson("{name:1}"); // false — unquoted key
isUrl("https://example.com"); // true
isUrl("http://example.com:8080/p?q"); // true
isUrl("google.com"); // false — no scheme
isUrl("ftp://example.com"); // false — wrong scheme
isUrl("https://google."); // false — empty hostname label
isUrl("https://google..com"); // false — empty middle label
isEmail("[email protected]"); // true
isEmail("[email protected]"); // true
isEmail("[email protected]"); // true
isEmail("a@b"); // false — no TLD with ≥ 2 letters
isEmail(["[email protected]"]); // false — must be a string
isJsonrejects primitive JSON by design.JSON.parse("123")succeeds butisJson("123")returnsfalsebecause the first character isn't{or[. If you need to accept primitives, callJSON.parseinside atry/catchdirectly.
Object-kind checks
Five predicates for specific built-in object types. Use them when you need to branch on what kind of object you received — a Promise, a Date, a form node, etc.
| Export | Rule |
|---|---|
| isPromise(value) | value instanceof Promise — accepts subclasses too. |
| isDate(value) | value instanceof Date. Invalid dates (new Date("nope")) still count. |
| isGenerator(value) | Duck-typed: object + .next function + self-iterable. |
| isFormElement(value) | value instanceof HTMLFormElement. SSR-safe — returns false when HTMLFormElement is undefined. Aliased as Is.form. |
| isFormData(value) | value instanceof FormData. |
import { isPromise, isDate, isGenerator, isFormElement, isFormData } from "@mongez/supportive-is";
isPromise(Promise.resolve()); // true
isPromise(fetch("/api")); // true
isPromise(async () => 1)(); // true — async fn returns a Promise
isPromise({ then() {} }); // false — thenable is not enough
isDate(new Date()); // true
isDate(new Date("not real")); // true — invalid Date is still a Date
isDate("2024-01-01"); // false — string, not Date
isDate(Date.now()); // false — number, not Date
function* count() { yield 1; }
isGenerator(count()); // true
isGenerator(count); // false — that's the generator function
isFormElement(document.createElement("form")); // true
isFormElement(document.createElement("div")); // false
isFormData(new FormData()); // true
isDatereturnstruefor invalid dates.new Date("not real")is still aDateinstance — the timestamp isNaN, but the type is intact. If you need "is it a valid Date", combine withNumber.isFinite(value.getTime()):function isValidDate(v: unknown): v is Date { return isDate(v) && !Number.isNaN((v as Date).getTime()); }
Environment checks
Predicates that read navigator, window, and document. They're safe to import on the server — none of them touch a global at module-eval. They throw on the server if you call them without a polyfill.
| Export | Reads | Use for |
|---|---|---|
| isMobile.android() / .ios() / .iphone() / .ipad() / .ipod() / .windows() / .any() | navigator.userAgent | Device-class branching. |
| isDesktop() | navigator.userAgent | !isMobile.any(). |
| isMac() | navigator.userAgent | Cmd-vs-Ctrl keyboard hints. |
| isBrowser(name) | Vendor-specific globals | One entry point, takes "chrome" \| "safari" \| "firefox" \| "opera" \| "edge" \| "ie". |
| isChrome() / isFirefox() / isSafari() / isOpera() / isIE() / isEdge() | Vendor-specific globals | Direct vendor probes (faster than isBrowser). |
import { isMobile, isMac, isDesktop, isBrowser, isChrome } from "@mongez/supportive-is";
isMobile.any(); // true on phones (Android/iOS/Windows Phone)
isMobile.iphone(); // narrower
isDesktop(); // !isMobile.any()
isMac(); // true on macOS
isChrome(); // true in Chrome
isBrowser("firefox"); // same as isFirefox()
isBrowser("Safari"); // case-insensitiveisMobile.any() reads navigator.userAgent every call, which is cheap but adds up in tight render loops — cache the result if you reference it more than a couple of times per render.
SSR safety
// Module top — safe everywhere
import { isMobile, isMac } from "@mongez/supportive-is";
// Call time — throws on the server because `navigator` is undefined
export function DeviceClass() {
return isMobile.any(); // ← throws in Node without a polyfill
}Gate the call when the same code runs in both environments:
const onClient = typeof window !== "undefined";
const device = onClient ? (isMobile.any() ? "mobile" : "desktop") : "unknown";For server-side UA-aware rendering in Next.js / Remix / TanStack Start, parse request.headers.get("user-agent") yourself with a dedicated UA parser — these predicates are written for the browser, not for the request-handler path.
Recipes
Validate a form input is a valid email
Reach for this when a form has a required email field and you want a single function that returns a Record<field, message> of errors. Combine isEmpty (for the required check) with isEmail (for format) — and use isUrl for optional URL fields the same way.
import { isEmail, isEmpty, isUrl } from "@mongez/supportive-is";
type ContactForm = {
name?: string;
email?: string;
website?: string;
};
function validate(form: ContactForm) {
const errors: Partial<Record<keyof ContactForm, string>> = {};
if (isEmpty(form.name)) errors.name = "Name is required";
if (isEmpty(form.email)) {
errors.email = "Email is required";
} else if (!isEmail(form.email!)) {
errors.email = "Email is invalid";
}
// Website is optional, but if present it must be a full URL.
if (!isEmpty(form.website) && !isUrl(form.website!)) {
errors.website = "Website must be a full http(s) URL";
}
return errors;
}
validate({ name: "", email: "[email protected]" });
// { name: "Name is required" }
validate({ name: "Hasan", email: "not-an-email" });
// { email: "Email is invalid" }Branch logic based on environment
Reach for this when the same component needs different padding, different keyboard hints, or different navigation chrome on mobile vs desktop.
import { isMac, isMobile } from "@mongez/supportive-is";
// Cache once at module-eval (or per render) — `isMobile.any()` re-reads the UA on every call.
const onMobile = isMobile.any();
const modKey = isMac() ? "Cmd" : "Ctrl";
export function CommandBar() {
return (
<div style={{ padding: onMobile ? 12 : 24 }}>
{onMobile ? <MobileNav /> : <DesktopNav />}
<kbd>{modKey} + K</kbd> to open the command palette
</div>
);
}If your bundle runs in SSR, gate the call sites with
typeof window !== "undefined"or move the read into auseEffect—isMobile.any()throws whennavigatorisn't defined.
A deep merge that respects class instances
Reach for this when you want to merge nested config objects but not merge into Date, Map, RegExp, or class instances — those should be replaced wholesale. isPlainObject returns false for all of them.
import { isPlainObject } from "@mongez/supportive-is";
function deepMerge<T extends object>(target: T, source: Partial<T>): T {
for (const key of Object.keys(source) as (keyof T)[]) {
const next = source[key];
const current = target[key];
if (isPlainObject(next) && isPlainObject(current)) {
target[key] = deepMerge(current as object, next as object) as T[keyof T];
} else {
target[key] = next as T[keyof T];
}
}
return target;
}
deepMerge(
{ user: { name: "A", joined: new Date("2024-01-01") } },
{ user: { joined: new Date("2025-01-01") } },
);
// { user: { name: "A", joined: 2025-01-01 } }
// — the Date is replaced, not mergedFilter empty fields out of an outbound payload
Reach for this before sending a form payload to an API: drop the keys with no real value (so the server's PATCH semantics only touch what the user actually filled in), but keep 0 and false, which isEmpty correctly preserves.
import { isEmpty } from "@mongez/supportive-is";
function pickNotEmpty<T extends Record<string, unknown>>(input: T): Partial<T> {
const out: Partial<T> = {};
for (const k of Object.keys(input) as (keyof T)[]) {
if (!isEmpty(input[k])) out[k] = input[k];
}
return out;
}
pickNotEmpty({
name: "Hasan",
bio: "",
age: 0,
preferences: {},
newsletter: false,
website: null,
});
// { name: "Hasan", age: 0, newsletter: false }
// — empty string, empty object, and null are dropped
// — 0 and false are kept (they're real values)Safely coerce user input into a URL object
Reach for this when a user pastes a link into your app and you want to parse it before rendering. new URL(...) throws on bad input; isUrl gives you a fast pre-check, and the try/catch is the belt-and-suspenders pair.
import { isUrl } from "@mongez/supportive-is";
function safeUrl(input: string): URL | null {
if (!isUrl(input)) return null;
try {
return new URL(input);
} catch {
return null;
}
}
const u = safeUrl("https://example.com/search?q=mongez");
u?.searchParams.get("q"); // "mongez"
safeUrl("not a url"); // null
safeUrl("ftp://x.com"); // null — isUrl rejects non-http(s)For untrusted input, do the actual fetch / navigation inside a
try/catch.isUrlis a convenience filter, not a security gate. If you're rendering an<a href>on the client, also sanitize the hostname (blockjavascript:,data:,file:, etc —isUrlalready rejects those, but defense-in-depth never hurts).
Build a polymorphic "string-or-regex" search API
Reach for this when you want a single function that takes either a literal string or a pre-built regex — pretty much every "find" / "match" / "highlight" helper you'll ever write.
import { isRegex } from "@mongez/supportive-is";
function findAll(haystack: string, needle: string | RegExp): string[] {
const re = isRegex(needle) ? needle : new RegExp(needle, "g");
return Array.from(haystack.matchAll(re), (m) => m[0]);
}
findAll("alice bob alice", "alice"); // ["alice", "alice"]
findAll("alice bob alice", /\balice\b/g); // ["alice", "alice"]
findAll("v1.2.3 and v4.5.6", /v\d+/g); // ["v1", "v4"]Narrow a union type in TypeScript
Reach for this when you have a string | number (or similar) and want both branches typed correctly. The type-checking guards are built-in value is T predicates on unknown, so no wrapper is needed.
import { isString, isPlainObject } from "@mongez/supportive-is";
function format(v: string | number): string {
if (isString(v)) {
return v.toUpperCase(); // v narrowed to string
}
return v.toFixed(2); // v narrowed to number
}
function readConfig(input: unknown) {
if (!isPlainObject(input)) throw new Error("Expected a plain object");
// input is now typed as Record<string, unknown>
return input.port ?? 3000;
}Migrating from v1
The v1 surface was a single default export:
import Is from "@mongez/supportive-is";
Is.string("x");
Is.empty([]);v2 keeps that import working — Is is still exported as the default — but each predicate is also a named export, so bundlers can drop the ones you don't use:
// v2 (preferred — tree-shakable)
import { isString, isEmpty } from "@mongez/supportive-is";
isString("x");
isEmpty([]);The exported Is object collects the canonical predicates from this package; methods removed in v2 (Is.cssSelector, Is.htmlTag, Is.callable, …) stay removed.
Named imports are roughly 80 bytes per predicate after minify+gzip; the whole
Isnamespace is ~3 KB. If you only use one or two predicates, named imports cut the cost by ~95%.
TypeScript
The package ships its types from source — no separate @types/ package needed. Every export is typed, and you can re-import the legacy namespace as a type when you need to:
import { isEmpty, isPlainObject, Is } from "@mongez/supportive-is";
import type { default as IsType } from "@mongez/supportive-is";
const ns: typeof Is = Is; // value reference
type Namespace = typeof IsType; // type-only reference (e.g. for generics)Type predicates
Every checker takes unknown. The ones whose runtime test is exactly "is this a T" are declared as value is T type predicates, so tsc narrows both branches:
| Function | Narrows unknown to |
|---|---|
| isString | string |
| isPlainObject | Record<string, unknown> |
| isObject | object |
| isDate | Date |
| isRegex | RegExp |
| isPromise | Promise<unknown> |
| isIterable | Iterable<unknown> |
| isGenerator | IterableIterator<unknown> (duck-typed: next() plus self-iterable) |
| isScalar | string \| boolean \| number \| symbol \| bigint |
| isPrimitive | string \| boolean \| number \| bigint |
| isFormElement | HTMLFormElement (object when the DOM lib is not in your compilation) |
| isFormData | FormData (object when FormData is not in your compilation) |
import { isPlainObject, isDate, isPromise } from "@mongez/supportive-is";
function describe(input: unknown) {
if (isPlainObject(input)) return Object.keys(input); // Record<string, unknown>
if (isDate(input)) return input.toISOString(); // Date
if (isPromise(input)) return input.then(String); // Promise<unknown>
return String(input);
}isInt, isFloat, isNumeric, isJson, isUrl, isEmail, isValidId, isEmpty and the environment checks (isMobile, isBrowser, ...) return plain boolean. They accept a subset of their base type, so a predicate would narrow the false branch wrongly: with n: number, if (isInt(n)) {} else { n } would make n never although 1.5 lands in the else branch. isNumeric also accepts numeric strings and single-element arrays (String([5]) is "5"), so it cannot be typed as number either.
Caveats: isObject and isPlainObject are structural, so a function-typed union member is dropped from the false branch of isObject even though it returns false for functions. isRegex checks constructor.name === "RegExp", so a RegExp subclass with a different class name returns false.
Upgrading from 2.x: the runtime is identical, but the types are stricter. Values typed any are narrowed by a type predicate, so if (isPlainObject(data)) { data.user.name } on an any value now fails with "'data.user' is of type 'unknown'". Type the value (const data: UserPayload) or narrow further after the guard.
Quick rules
- One predicate = one named import. Don't import the default
Isnamespace just for tree-shaking convenience — bundlers can't statically prove which keys you'll touch on a namespace object. - No runtime dependencies. Every predicate is one short function. If you need date math, regex composition, or schema validation, use the right tool — not this package.
- DOM predicates run lazily. Importing
isMobileon the server is safe; callingisMobile.android()on the server throws becausenavigatorisn't defined. - Predicates are not validators.
isUrl/isEmail/isJsonare convenience filters. For trust decisions (open the link / send to address / interpret as JSON), do the actual operation in atry/catchor pair withzod/valibot. isEmptykeeps0,false, andnew Date(). Usevalue == null || value === ""directly if you want a stricter "nullish-or-blank" check.
Related packages
| Package | Use when you need |
|---|---|
| @mongez/reinforcements | The transformations you reach for after the predicate passes — object/string/array helpers (get, set, clone, slugify, groupBy, …). |
| @mongez/atom | Framework-agnostic reactive state — pairs with isPlainObject for safe atom payloads. |
| @mongez/events | Tiny event bus. |
| @mongez/dotenv | Typed .env loader — pairs naturally with isEmpty(env("X")) checks at boot. |
For deeper validation (schema, transformation, custom error shapes), use zod or valibot directly — those are full validation libraries and a different scope than the predicates here.
Further reading
CHANGELOG.md— release notes and the v1 → v2 migration list.llms-full.txt— exhaustive single-file API surface for tool-assisted development.skills/— per-topic deep-dives (primitives, collections, formats, misc, environment, recipes).
License
MIT — see LICENSE.
