@sladg/what-that
v0.2.0
Published
Tiny, dependency-free type guards: is.object(), is.not.number()
Maintainers
Readme
@sladg/what-that
Tiny, dependency-free type guards and comparators with an ergonomic is.* namespace. Every check is a TypeScript type predicate — it narrows, it composes, and impossible comparisons are compile errors.
- Zero dependencies, ~3 kB gzip
- ESM + CJS,
sideEffects: false, target es2020 - Strict types everywhere — guards narrow via predicates, negations narrow via
Exclude, comparators reject cross-type mixes at compile time
npm install @sladg/what-thatimport { is } from '@sladg/what-that'Type guards — is.*
Every guard is (value: unknown) => value is T:
is.string(x) // x is string
is.number(x) // x is number
is.boolean(x) // x is boolean
is.symbol(x) // x is symbol
is.function(x) // x is (...args: unknown[]) => unknown
is.array(x) // x is unknown[]
is.object(x) // x is Record<string, unknown> — plain objects only:
// not arrays, Date, Map, class instances (proxies of plain objects pass)
is.objectOrArray(x) // x is Record<string, unknown> | unknown[]
is.date(x) // x is Date
is.regexp(x) // x is RegExp
is.map(x) // x is Map<unknown, unknown>
is.set(x) // x is Set<unknown>
is.weakMap(x) // x is WeakMap<object, unknown>
is.weakSet(x) // x is WeakSet<object>
is.promise(x) // x is Promise<unknown>
is.error(x) // x is Error — subclasses (TypeError, custom) included
is.iterable(x) // x is Iterable<unknown> — strings, arrays, Map, Set, generators
is.nil(x) // x is null | undefined
is.null(x) // x is null
is.undefined(x) // x is undefined
is.primitive(x) // x is string | number | boolean | bigint | symbol | null | undefined
is.numericKey(k) // string is a non-negative integer index ("0", "42")const data: unknown = await fetchSomething()
if (is.object(data)) {
data.name // ok — data is Record<string, unknown>
}is.instanceOf(value, Ctor)
Narrows to InstanceType of any constructor, abstract classes included:
if (is.instanceOf(err, HttpError)) {
err.statusCode // ok
}Negations — is.not.*
Every guard has a mirror that narrows by exclusion (Exclude<T, …>):
const value: string | number = getValue()
if (is.not.string(value)) {
value.toFixed(2) // value is number
}
const items = [1, null, 2, undefined].filter(is.not.nil)
// items: number[]is.not.instanceOf(x, Ctor) excludes the instance type from unions the same way.
Emptiness — is.empty / is.not.empty
One overloaded function instead of x.length === 0, Object.keys(x).length === 0, x === '', x.size === 0:
is.empty(null) // true — nil is empty
is.empty('') // true
is.empty([]) // true
is.empty({}) // true — own properties only
is.empty(new Map()) // true — size-based
is.empty(new Set()) // true
is.empty(0) // typed as literal false — numbers are never empty
is.empty(new Date()) // typed as literal falseThe types work as hard as the runtime:
declare const name: string | null
if (is.empty(name)) {
name // '' | null
} else {
name // string
}
declare const items: number[]
if (is.not.empty(items)) {
items[0] // number — narrowed to [number, ...number[]], no undefined
}Deep equality — is.equal / is.not.equal
Structural comparison for arrays, plain objects, Date (by time), RegExp (by source and flags), Map (keys by identity, values deeply), Set (by membership):
is.equal({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] }) // true
is.equal(new Date('2024-01-01'), new Date('2024-01-01')) // true
is.equal(new Set([1, 2]), new Set([2, 1])) // true
is.equal(new Map([['a', { x: 1 }]]), new Map([['a', { x: 1 }]])) // trueComparisons — is.that()
Fluent and unambiguous: the subject goes inside that(), the reference is the argument. Strict mode compares same types only — number, bigint, string (lexicographic), Date:
is.that(price).largerThan(0)
is.that(count).atLeast(10) // >=
is.that(retries).atMost(3) // <=
is.that(age).between(18, 65) // inclusive
is.that('b').smallerThan('c') // lexicographic
is.that(startDate).smallerThan(endDate)
is.that(5).largerThan(new Date()) // ❌ compile error — cross-typeLiteral subjects widen properly: is.that(0).between(1, 5) type-checks and returns false.
Loose comparisons — is.loosely.that()
Cross-representation comparison. The subject picks the domain, so it's never everything-vs-everything:
Numeric subject (number, bigint, `${number}` strings) — compares against numbers, bigints, numeric strings, and Dates as unixMs:
is.loosely.that('42').largerThan(7) // true — numeric, not lexicographic
is.loosely.that(5n).atLeast('5') // true — bigint vs numeric string
is.loosely.that(3).smallerThan(5n) // true — bigint vs number, exact per spec
// No float runoff: integer strings ride BigInt beyond 2^53
is.loosely.that('9007199254740993').largerThan('9007199254740992') // trueDate subject — accepts any date representation (Date, parseable string, unixMs number/bigint), always compared by instant in UTC:
const deadline = new Date('2024-06-01')
is.loosely.that(deadline).between('2024-01-01', '2024-12-31') // true
is.loosely.that(deadline).largerThan(1704067200000) // vs unixMsString subject — compares against strings and Dates. Two parseable date strings compare by instant (cross-format, cross-timezone); plain strings fall back to lexicographic:
is.loosely.that('2024-01-01T12:00:00+02:00').largerThan('2024-01-01T09:00:00Z') // true — 10Z > 9Z
is.loosely.that('banana').largerThan('apple') // true — lexicographic
is.loosely.that('banana').largerThan(5) // ❌ compile error — string vs number
is.loosely.that({}) // ❌ compile error — objects are not comparableWhat it will not do
- Detect proxies — impossible in JS by design; a Proxy of a plain object passes
is.object(), which is what you want - Schema validation with messages — that's zod's job
- Locale-aware anything — string order is code-unit lexicographic
License
MIT
