
Security News
Suno Breached via Shai-Hulud Worm, Leaked Code Exposes AI Music Scraping
A Shai-Hulud infection exposed Suno's source code, which shows the AI music startup stream-ripped tracks to train its models.
@mongez/supportive-is
Advanced tools
Tree-shakable type & shape predicates: isString, isEmpty, isUrl, isJson, isEmail, isPromise, isMobile, … framework-agnostic and dependency-free.
Tree-shakable type and shape predicates for JavaScript — isString, isEmpty, isUrl, isPromise, isMobile.any() — one named export each, zero runtime dependencies.
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("user@example.com"); // 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"); // true
| 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 | isString is a real value is string type guard out of the box. |
npm install @mongez/supportive-is
yarn add @mongez/supportive-is
pnpm add @mongez/supportive-is
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("user@example.com"); // 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.
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")); // true
isStringis the only one that narrows in TypeScript out of the box. The others are typed as(value: any) => booleanbecause the parameter isany. Wrap them in your own typed guard (function isObjLike<T extends object>(v: unknown): v is T { return isPlainObject(v); }) when you need narrowing on union types.
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({}); // false
isPlainObject 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.
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.
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 */ } // idiomatic
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("user@example.com"); // true
isEmail("a.b.c@example.co.uk"); // true
isEmail("user+tag@example.com"); // true
isEmail("a@b"); // false — no TLD with ≥ 2 letters
isEmail(["x@y.com"]); // 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.
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()); }
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-insensitive
isMobile.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.
// 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.
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: "u@x.com" });
// { name: "Name is required" }
validate({ name: "Hasan", email: "not-an-email" });
// { email: "Email is invalid" }
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.
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 merged
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)
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).
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"]
Reach for this when you have a string | number (or similar) and want both branches typed correctly. isString is a built-in value is string guard. For the other predicates the parameter is typed as any, so you wrap them in your own type guard.
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
}
// For predicates without built-in narrowing, wrap your own:
function isRecord(v: unknown): v is Record<string, unknown> {
return isPlainObject(v);
}
function readConfig(input: unknown) {
if (!isRecord(input)) throw new Error("Expected a plain object");
// input is now typed as Record<string, unknown>
return input.port ?? 3000;
}
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%.
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)
Most predicates are typed as (value: any) => boolean because they accept anything you can hand them. The exception is isString, which TypeScript narrows to value is string automatically:
import { isString } from "@mongez/supportive-is";
function format(v: string | number): string {
if (isString(v)) return v.toUpperCase(); // v: string
return v.toFixed(2); // v: number
}
For the others, wrap them in your own typed guard when you need narrowing on a union:
import { isPlainObject, isPromise, isDate } from "@mongez/supportive-is";
function isRecord(v: unknown): v is Record<string, unknown> {
return isPlainObject(v);
}
function isThenable<T>(v: unknown): v is Promise<T> {
return isPromise(v);
}
function isDateInstance(v: unknown): v is Date {
return isDate(v);
}
This wrapping pattern is one line, and you only need to do it once per predicate per project. The wrapper compiles to a single call — there's no runtime cost.
Is namespace just for tree-shaking convenience — bundlers can't statically prove which keys you'll touch on a namespace object.isMobile on the server is safe; calling isMobile.android() on the server throws because navigator isn't defined.isUrl / isEmail / isJson are convenience filters. For trust decisions (open the link / send to address / interpret as JSON), do the actual operation in a try/catch or pair with zod/valibot.isEmpty keeps 0, false, and new Date(). Use value == null || value === "" directly if you want a stricter "nullish-or-blank" check.| 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.
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).MIT — see LICENSE.
FAQs
Tree-shakable type & shape predicates: isString, isEmpty, isUrl, isJson, isEmail, isPromise, isMobile, … framework-agnostic and dependency-free.
The npm package @mongez/supportive-is receives a total of 197 weekly downloads. As such, @mongez/supportive-is popularity was classified as not popular.
We found that @mongez/supportive-is demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
A Shai-Hulud infection exposed Suno's source code, which shows the AI music startup stream-ripped tracks to train its models.

Security News
Vercel is formalizing a monthly release program for Next.js. The change follows React2Shell and a sharp rise in AI-assisted vulnerability discovery.

Research
/Security News
11 malicious NuGet tools pose as game cheats to deploy Windows payloads, track hosts, and use Google Sheets for telemetry and control.