Skip to content

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

The type-checking guards are declared type predicates on unknown, so no wrapper is needed:

import { isString, isPlainObject } from "@mongez/supportive-is";
function greet(v: string | number) {
if (isString(v)) {
return `Hello, ${v.toUpperCase()}`; // v is narrowed to string here
}
return `Number: ${v.toFixed(2)}`; // v is narrowed to number here
}
function readPort(input: unknown) {
if (!isPlainObject(input)) throw new Error("Expected a plain object");
return input.port ?? 3000; // input: Record<string, unknown>
}
FunctionNarrows unknown to
isStringstring
isPlainObjectRecord<string, unknown>
isObjectobject
isDateDate
isRegexRegExp
isPromisePromise<unknown>
isIterableIterable<unknown>
isGeneratorIterableIterator<unknown> (duck-typed: next() plus self-iterable)
isScalarstring | boolean | number | symbol | bigint
isPrimitivestring | boolean | number | bigint
isFormElementHTMLFormElement (object when the DOM lib is not in your compilation)
isFormDataFormData (object when FormData is not in your compilation)

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.