On This Page
Coercion Utilities
The Coercion utilities turn loose input — attribute strings, query params, form values, JSON — into the type you actually want. Each returns the target type or null when the input has no clean representation, so a failed coercion is a value you can guard with ??, never an Invalid Date, NaN, or "[object Object]" that poisons code downstream.
const page = toNumber(params.get('page')) ?? 1;const when = toDate(input.value) ?? new Date();Each helper is also exported as coerceBoolean, coerceNumber, coerceInteger, coerceDate, coerceDuration, coerceByteSize, coerceBytes, and coerceString for callers who think in coercion terms.
Every helper accepts an onInvalid setting. It defaults to 'null' (the failed coercion returns null, composing with ??). Pass { onInvalid: 'passthrough' } to return the original value instead, so a schema or validator can flag the bad input rather than see an erasing null — and the TypeScript return widens to include the input type (number | string for toNumber(str, { onInvalid: 'passthrough' })).
Functions
toBoolean
function toBoolean(value, { truthy, falsy, loose = false, onInvalid = 'null' } = {})Coerces a value to a boolean, or null when it recognizes no boolean reading. Booleans pass through, numbers read by zero-ness, and strings match a generous set case-insensitively: true, t, yes, y, on, enabled read as true, and false, f, no, n, off, disabled, null, undefined, nan read as false, alongside numeric strings by their value. Anything else returns null.
Parameters
| Name | Type | Description |
|---|---|---|
| value | unknown | The value to coerce |
| settings | object | Optional configuration |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| truthy | string | string[] | — | Extra tokens to treat as true for this call, winning over the config vocabulary |
| falsy | string | string[] | — | Extra tokens to treat as false for this call, winning over the config vocabulary and a truthy match |
| loose | boolean | false | Coerce unrecognized input via native truthiness instead of returning null |
| onInvalid | ‘null’ | ‘passthrough’ | ‘null’ | How an unrecognized value resolves (loose takes precedence when both are set) |
Returns
true, false, or null if unrecognized. Never null under loose.
Example
import { toBoolean } from '@semantic-ui/utils';
console.log(toBoolean('yes')); // trueconsole.log(toBoolean('off')); // falseconsole.log(toBoolean('false')); // false (not the native Boolean('false') === true)console.log(toBoolean('1')); // true
// unrecognized input is null, so it composes with ??console.log(toBoolean('banana')); // nullconsole.log(toBoolean('banana') ?? false); // false
// loose falls back to native truthiness, never nullconsole.log(toBoolean('banana', { loose: true })); // true
// extend the falsy set without losing the defaultsconsole.log(toBoolean('nope', { falsy: ['nope'] })); // falsetoBoolean.config
toBoolean.config holds the recognized vocabulary (truthy, falsy) and the defaults for loose and onInvalid. Edit it once at app boot and every call inherits it, so a locale or domain adds its own spellings in one place instead of passing truthy/falsy on every call. Per-call settings still win over it.
import { toBoolean } from '@semantic-ui/utils';
// teach it another language, oncetoBoolean.config.truthy.push('oui', 'sí');toBoolean.config.falsy.push('non', 'nein');
console.log(toBoolean('oui')); // trueconsole.log(toBoolean('non')); // false
// flip a default for every calltoBoolean.config.onInvalid = 'passthrough';console.log(toBoolean('banana')); // 'banana'toNumber
function toNumber(value, { onInvalid = 'null' } = {})Coerces a value to a finite number, or null when it can’t be converted. Numeric strings parse via Number (so hex, scientific, and surrounding whitespace are fine), booleans read as 1 and 0, and NaN, Infinity, blanks, unit-tagged strings, arrays, and objects return null. It never hands back NaN or Infinity.
Parameters
| Name | Type | Description |
|---|---|---|
| value | unknown | The value to coerce |
| settings | object | Optional configuration (onInvalid, see above) |
Returns
The finite number, or null if unconvertible.
Example
import { toNumber } from '@semantic-ui/utils';
console.log(toNumber('3.14')); // 3.14console.log(toNumber(' 42 ')); // 42console.log(toNumber('5px')); // nullconsole.log(toNumber('1e999')); // null (overflow to Infinity)console.log(toNumber(true)); // 1
console.log(toNumber('abc') ?? 0); // 0toInteger
function toInteger(value, { onInvalid = 'null' } = {})Coerces a value to an integer by truncating toward zero, or null when it’s unconvertible or non-finite. Equivalent to toNumber followed by a truncation, so Infinity joins the null bucket. Under { onInvalid: 'passthrough' } it returns the original value, never a truncated NaN.
Parameters
| Name | Type | Description |
|---|---|---|
| value | unknown | The value to coerce |
| settings | object | Optional configuration (onInvalid, see above) |
Returns
The truncated integer, or null if unconvertible.
Example
import { toInteger } from '@semantic-ui/utils';
console.log(toInteger('3.9')); // 3console.log(toInteger('-3.9')); // -3console.log(toInteger(42.7)); // 42console.log(toInteger(Infinity)); // nulltoDate
function toDate(value, { onInvalid = 'null', epoch = 'milliseconds' } = {})Coerces a value to a Date, or null when it can’t be parsed — never an Invalid Date. Accepts a Date, a number read as epoch milliseconds, an ISO-8601 string, any value that hands its instant over through toJSDate(), or a Temporal value holding an instant, an Instant or a ZonedDateTime. A PlainDate, a PlainTime or a Duration holds no instant and reads as null. Ambiguous or locale-dependent spellings return null rather than a guessed date: bare years, slash and text dates, and day overflow all reject, so '01/15/2024' and '2024-02-30' never become a wrong Date. A zoneless datetime resolves in the ambient timezone (the user’s zone on the client) and is returned as a UTC instant, the same as native new Date on an <input type="datetime-local"> value. A date-only string is read as UTC, and a timestamp passed as a string is rejected, so pass a number for epoch ms.
Parameters
| Name | Type | Description |
|---|---|---|
| value | unknown | The value to coerce |
| settings | object | Optional configuration |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| epoch | ‘milliseconds’ | ‘seconds’ | ‘milliseconds’ | How a number reads. Pass 'seconds' for unix-second timestamps (a JWT exp), where the millisecond reading would produce a valid but wrong 1970 date |
| onInvalid | ‘null’ | ‘passthrough’ | ‘null’ | How a failed coercion resolves (see above) |
Returns
The Date, or null if unparseable.
Example
import { toDate } from '@semantic-ui/utils';
console.log(toDate('2024-01-01')); // Date for 2024-01-01console.log(toDate(1700000000000)); // Date from epoch msconsole.log(toDate(payload.exp, { epoch: 'seconds' })); // Date from a unix-second JWT expconsole.log(toDate('01/15/2024')); // null (ambiguous format)console.log(toDate('banana')); // null (never an Invalid Date)console.log(toDate(Temporal.Now.instant())); // Date for the same instantconsole.log(toDate(Temporal.ZonedDateTime.from('2026-09-06T23:30:00+09:00[Asia/Tokyo]'))); // Date for the same instantconsole.log(toDate(Temporal.PlainDate.from('2026-09-06'))); // null (a calendar day holds no instant)console.log(toDate({ toJSDate: () => new Date(0) })); // Date at the epoch, through toJSDate()
const when = toDate(userInput) ?? new Date();toDuration
function toDuration(value, { onInvalid = 'null' } = {})Coerces a duration expression to milliseconds, or null when it reads as no duration at all. A number is already milliseconds, and a string takes one number followed by one optional unit, case-insensitively, with an optional space between the two: '5s', '1.5h', '10 minutes', '2hrs', '300msecs', '.5d'. Leave the unit off and the number reads as milliseconds, so '1500' and 1500 agree.
The sign is kept, so '-1.5h' is -5400000. Compound expressions like '1h 30m' are a different grammar and return null rather than a partial reading, as do unknown units, bare numbers with trailing junk, and exponent notation.
formatDuration is the inverse, printing milliseconds back in this grammar (300000 as '5m'), so a config can read '5m' on the way in and show '5m' on the way out.
Units
| Unit | Accepted spellings |
|---|---|
| Milliseconds | ms, msec, msecs, millisecond, milliseconds |
| Seconds | s, sec, secs, second, seconds |
| Minutes | m, min, mins, minute, minutes |
| Hours | h, hr, hrs, hour, hours |
| Days | d, day, days |
| Weeks | w, week, weeks |
Every unit here is a fixed span. Years and months are absent because neither has one: a year is a judgment call (the ms package reads it as 365.25 days) and a month has no length without a calendar date to anchor it. Name your own value in toDuration.config.units rather than inheriting someone else’s guess.
Parameters
| Name | Type | Description |
|---|---|---|
| value | unknown | The value to coerce |
| settings | object | Optional configuration (onInvalid, see above) |
Returns
The duration in milliseconds, or null if unreadable.
Example
import { toDuration } from '@semantic-ui/utils';
console.log(toDuration('5s')); // 5000console.log(toDuration('1.5h')); // 5400000console.log(toDuration('10 minutes')); // 600000console.log(toDuration(1500)); // 1500 (already milliseconds)console.log(toDuration('1500')); // 1500 (no unit reads as milliseconds)console.log(toDuration('1h 30m')); // null (compound expressions are a different grammar)
setTimeout(retry, toDuration(config.retryAfter) ?? 1000);toDuration.config
toDuration.config.units holds the milliseconds per unit, keyed by the lowercase spelling accepted after the number. Edit it once at app boot to teach the grammar a unit your domain uses, and every call inherits it.
import { toDuration } from '@semantic-ui/utils';
toDuration.config.units.y = 365 * 24 * 60 * 60 * 1000;toDuration.config.units.years = toDuration.config.units.y;
console.log(toDuration('1y')); // 31536000000toByteSize
function toByteSize(value, { onInvalid = 'null', base } = {})Coerces a byte size expression to a whole number of bytes, or null when it reads as no size at all. A number is already bytes, and a string takes one number followed by one optional unit, case-insensitively, with an optional space between the two: '10mb', '1.5 KB', '2 GiB', '.5gb'. Leave the unit off and the number reads as bytes, so '1500' and 1500 agree. The same grammar as toDuration, for the other quantity a config file spells with a unit.
The built-in units are abbreviations only, the grammar of the ecosystem’s bytes() package, where toDuration reads words because ms() does. Sizes in a config are spelled short, and every importer pays for each spelling in the table, so '10 megabytes' and '512k' are one toByteSize.config line rather than a built-in.
kb through pb scale by base, 1024 unless configured, because that is what every config that accepts '10mb' already means (nginx, Docker, upload limits). The IEC spellings kib through pib are 1024 by definition and never move. A byte is indivisible, so the result rounds to the nearest whole byte, which also folds the float noise a decimal multiplier leaves ('1.1mb' at base 1000 is 1100000, not 1100000.0000000002).
The sign is kept, so '-2mb' is a delta. Bits are not bytes and '10mbit' returns null, as do compound forms like '1mb 512kb', unknown units, and exponent notation. Matching is case-insensitive, so 'Mb' reads as megabytes, not megabits.
Units
| Unit | Scales by |
|---|---|
b |
1 |
kb |
base¹ |
mb |
base² |
gb |
base³ |
tb |
base⁴ |
pb |
base⁵ |
kib, mib, gib, tib, pib |
1024ⁿ, always |
Parameters
| Name | Type | Description |
|---|---|---|
| value | unknown | The value to coerce |
| settings | object | Optional configuration |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| base | number | toByteSize.config.base (1024) |
What kb/mb/gb scale by for this call, 1024 or 1000 |
| onInvalid | string | 'null' |
How a failed coercion resolves, see above |
Returns
The size in bytes, or null if unreadable.
Example
import { toByteSize, byteLength } from '@semantic-ui/utils';
console.log(toByteSize('10mb')); // 10485760console.log(toByteSize('1.5 KB')); // 1536console.log(toByteSize('10mib')); // 10485760 (whatever base says)console.log(toByteSize('10mb', { base: 1000 })); // 10000000console.log(toByteSize(1500)); // 1500 (already bytes)console.log(toByteSize('1h')); // null (a duration is a different quantity)
const maxUpload = toByteSize(config.maxUpload) ?? toByteSize('10mb');if (byteLength(body) > maxUpload) { reject(); }toByteSize.config
toByteSize.config.base is what the ambiguous units scale by, and toByteSize.config.units holds the exponent of that base per unit, keyed by the lowercase spelling accepted after the number. Values are exponents rather than multipliers so a new spelling is one line and follows the base. toByteSize.config.iecUnits is the same shape over 1024 and is never moved by base. Edit any of them once at app boot and every call inherits it.
import { toByteSize } from '@semantic-ui/utils';
toByteSize.config.base = 1000; // SI everywhere, to match a macOS-style displaytoByteSize.config.units.m = 2; // the nginx and Docker spellingtoByteSize.config.units.megabytes = 2;toByteSize.config.units.eb = 6;
console.log(toByteSize('1kb')); // 1000console.log(toByteSize('10m')); // 10000000console.log(toByteSize('1eb')); // 1000000000000000000toBytes
function toBytes(value, { onInvalid = 'null' } = {})Coerces a value to a Uint8Array, or null when it holds no bytes. A string reads as its UTF-8 text, a Uint8Array returns as the same reference, an ArrayBuffer or any other typed array or DataView becomes a view over the same memory bounded to the view (no copy), and an array of integers 0..255 is copied into a new array.
A string is always text, never an encoding. Decoding is fromBase64’s job, and the spelling that returns bytes from base64 is fromBase64(s, { as: 'bytes' }). A bare number returns null rather than reading as a length, and an array holding a non-byte ([300], [1.5]) returns null rather than wrapping it, since neither is a clean reading.
Parameters
| Name | Type | Description |
|---|---|---|
| value | unknown | The value to coerce |
| settings | object | Optional configuration (onInvalid, see above) |
Returns
The Uint8Array, or null if the value holds no bytes.
Example
import { toBytes } from '@semantic-ui/utils';
console.log(toBytes('héllo')); // Uint8Array [104, 195, 169, 108, 108, 111]console.log(toBytes(new Float32Array([1]))); // Uint8Array over the same 4 bytesconsole.log(toBytes([1, 2, 3])); // Uint8Array [1, 2, 3]console.log(toBytes([1, 2, 300])); // null (300 is not a byte)console.log(toBytes(5)); // null (a number is not a length)
await crypto.subtle.digest('SHA-256', toBytes(payload));toString
function toString(value, { loose = false, onInvalid = 'null' } = {})Coerces a value to a string, or null when there is no faithful string form. Strings pass through and a finite number, a boolean, or a bigint goes through String(). Objects, arrays, functions, symbols, non-finite numbers, and nullish return null, so a String field never silently stores "[object Object]" or "NaN". Under loose, objects and arrays render with JSON for display output, still returning null on unserializable input rather than throwing.
Parameters
| Name | Type | Description |
|---|---|---|
| value | unknown | The value to coerce |
| settings | object | Optional configuration |
Options
| Name | Type | Default | Description |
|---|---|---|---|
| loose | boolean | false | Render objects and arrays with JSON instead of returning null |
| onInvalid | ‘null’ | ‘passthrough’ | ‘null’ | How a value with no faithful string form resolves |
Returns
The string, or null if there is no faithful string form.
The loose output is a display string, not a round-trippable serialization — JSON drops undefined and functions and rewrites Date and NaN, so it should not be fed back through toDate or toNumber.
Example
import { toString } from '@semantic-ui/utils';
console.log(toString(42)); // '42'console.log(toString(true)); // 'true'console.log(toString({ a: 1 })); // nullconsole.log(toString({ a: 1 }, { loose: true })); // '{"a":1}'console.log(toString([1, 2], { loose: true })); // '[1,2]'