Recipes
Idiomatic compositions across @mongez/supportive-is predicates.
Form validation
import { isEmpty, isEmail, 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 must be a URL. if (!isEmpty(form.website) && !isUrl(form.website!)) { errors.website = "Website must be a full http(s) URL"; } return errors;}A deep merge that respects class instances
isPlainObject is the right predicate for “should I merge into this, or replace it?” — it returns false for Dates, Maps, Sets, and class instances, all of which want to be replaced, not merged.
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;}Filtering nullable / empty entries
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({ a: "x", b: "", c: null, d: 0 });// { a: "x", d: 0 } — note that 0 is kept (it's not empty)Mobile-aware UI without a state library
import { isMobile } from "@mongez/supportive-is";
// Read once and cache — `isMobile.any()` re-reads `navigator.userAgent` on// every call, which is cheap but adds up in tight render loops.const isMobileDevice = isMobile.any();
export function Card() { return ( <article style={{ padding: isMobileDevice ? 12 : 24 }}> ... </article> );}Cmd vs Ctrl for keyboard shortcuts
import { isMac } from "@mongez/supportive-is";
const MOD = isMac() ? "Cmd" : "Ctrl";
export function SaveHint() { return <kbd>{MOD} + S</kbd>;}Safe URL coercion
new URL("...") throws on bad input. Wrap with isUrl first:
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/page?q=1");u?.searchParams.get("q"); // "1"Polymorphic API — string or regex
import { isRegex } from "@mongez/supportive-is";
function find(haystack: string, needle: string | RegExp) { const re = isRegex(needle) ? needle : new RegExp(needle, "g"); return Array.from(haystack.matchAll(re), (m) => m[0]);}Type narrowing in TypeScript
None of the predicates ship as declared TypeScript type guards — they’re all typed (value: any) => boolean. Wrap them with your own typed guard when you need narrowing on a union:
import { isString, isPlainObject } from "@mongez/supportive-is";
function isStr(v: unknown): v is string { return isString(v);}
function isObjLike<T extends object>(v: unknown): v is T { return isPlainObject(v);}
function greet(v: string | number) { if (isStr(v)) { return `Hello, ${v.toUpperCase()}`; // v is narrowed to string here } return `Number: ${v.toFixed(2)}`; // v is narrowed to number here}The wrapper compiles to a single call — there’s no runtime cost.