Skip to main content

Types and Coercion

Files TransformScript has a small set of runtime value types and several distinct ways of converting between them. The conversion mechanisms deliberately differ from one another, so knowing which one applies in a given spot is the key to predicting what your script returns.

Runtime Types

These are the real values a script manipulates. Each can be tested with is, produced by typeOf, and, where it makes sense, targeted with as.

TypeWhat It IsLiteral Example
Stringtext"abc"
Numberinteger or decimal42, 42.5
Booleantrue / falsetrue
Nullabsence of a valuenull
Arrayordered list[1, 2, 3]
Objectkey/value map{ a: 1 }
Regexregular expression/foo/i
Datecalendar date, no time|2024-05-01|
DateTimezoned instant|2020-10-01T23:57:59-07:00|
LocalDateTimedate + time, no zone|2020-10-01T23:57:59|
LocalTime / Timetime of day|23:57:59|
PeriodISO-8601 duration|P1D|, |PT3H|
TimeZonea time zone(no literal; via coercion)

typeOf(x) returns one of these names. LocalTime and Time are the same underlying type, aliased to each other. Functions are reflectable, so typeOf(aLambda) is "Function", but Function is not a value you can write as a literal or coerce to.

Binary and Uri are not standalone value types. Binary data is just a String of bytes, so there is nothing to construct or cast to: x as Binary and x as Uri raise an error.

Temporal Types

The temporal types are first-class values, not strings.

Date is a calendar date. LocalTime is a wall-clock time with hour, minute, second, and optional fractional seconds. LocalDateTime pairs a Date and a LocalTime with no zone. DateTime is a zoned instant.

Period is an ISO-8601 duration with fields for years, months, weeks, days, hours, minutes, and seconds. Periods add and subtract field-wise, so |P1D| + |PT3H| gives |P1DT3H|, and they apply to temporals, so |2021-03-02T10:39:59| - |P1D| gives |2021-03-01T10:39:59|. Applying time-of-day components to a bare Date raises an error, because a Date has no time to adjust.

Coercion: Four Mechanisms

TransformScript coerces values in four different places, and they do not all agree. Each is described separately because the differences matter.

Truthiness in Conditionals and Filters

When a value is used as a condition, such as in a ternary, an if, a filter selector, or an if-guarded object entry, it is tested for truthiness. The rule is strict about strings: a string is truthy only if it equals "true" (case-insensitive, trimmed). So "yes", "0", and "1" are all falsy; only "true" is truthy. Numbers are truthy when non-zero. null and false are falsy. Everything else is truthy.

The boolean operators and, or, and not are stricter still. They require actual booleans and raise an error on anything else.

Explicit Cast With as Type

expr as Type { options } converts using the target type's rules. If the value already matches and you give no options, it passes through unchanged.

  • 42 as String gives "42". "42" as Number gives 42, an integer, with the integer type preserved. "false" as Boolean gives false.
  • as Number parses integer-looking strings to integers, supports locale separators (decimalSeparator, groupingSeparator, locale), and raises an error on empty input.
  • as Boolean accepts true/t/yes/y/1 and false/f/no/n/0/null/"", and raises an error on anything else. You can customize the accepted values with trueValues and falseValues.
  • as String keeps null as null, formats temporals (honoring a format strftime string and timeZone), and JSON-encodes arrays and objects.
  • Temporal casts accept a format (strptime) and convert among temporal types. Number as DateTime treats the number as epoch seconds.

Unknown option keys, or casting to a type that is not a runtime type, raise an error.

Conversion Functions Such as toString and toNumber

The toX functions are separate from as Type and differ in their edge cases.

  • toString(null) gives "", whereas null as String gives null.
  • toNumber(x) always returns a decimal, so toNumber("42") gives 42.0, unlike as Number, which preserves integers.
  • toBoolean(x) never raises an error. It accepts 1/yes/0/no and falls back to truthiness, unlike as Boolean, which raises on unrecognized strings.
  • toDecimal, toBinary (which produces a String of bytes), and the temporal toDate, toDateTime, toLocalDateTime, and toTime round out the set.

Implicit Coercion in Operators

Arithmetic operators coerce operands to numbers: a numeric string becomes a number, true becomes 1, false and null become 0, and a non-numeric string raises an error. The notable overload is +. If either side is a string, the result is string concatenation, so 1 + "a" gives "1a". For arrays, + concatenates or pushes. The strict ++, <<, and >> operators require matching operand types.

Equality and Ordering

== and != are strict and do no coercion. A text-only object compares equal to the equivalent plain string.

~= is coercive equality. It coerces one operand toward the other's shape and then compares, so "true" ~= true is true and "1" ~= 2 is false. It coerces recursively through arrays and objects.

The relational operators >, >=, <, and <= are left-type-driven. They return false if either operand is null. Otherwise they coerce the right operand to the left operand's type and compare. If the right side cannot be coerced to match, the result is false.