Casts
You can add casts from native types (or any types supported by the type system, such as literals) to your custom type, allowing fields receiving your type to also accept the casted-from types.
This is done through the casts: (cast1, cast2, ...)
parameter. Each cast takes at least (from: typename, with: constructor => value => constructor(...))
, where value
was already casted to typename
beforehand (e.g. if typename
is float, then value
will always have type float
, even if the user passes an integer). It may optionally take check: value => bool
as well to only accept that typename
if check(value)
is true
.
In the future, automatic casting from dictionaries will be supported (although it can already be manually implemented).
Note: Casts are ordered. This means that specifying a cast from
int
and thenfloat
is different from specifying a cast fromfloat
followed byint
, for example. This is relevant when two or more types in the union can accept the same native type, with differing checks or casts. In the case ofint
andfloat
, the integer5
will trigger the cast fromint
as you'd expect if theint
cast comes first, but will converted to5.0
before triggering the cast fromfloat
if thefloat
cast is specified first. (Of course, a float such as4.0
will trigger the cast fromfloat
in both cases, since it isn't accepted byint
). Keep this in mind!
These principles are made evident in the example below:
#import "@local/elembic:0.0.1" as e: field, types
#let person = e.types.declare(
"person",
prefix: "@preview/my-package,v1",
fields: (
field("name", str, doc: "Person's name", required: true),
field("age", int, doc: "Person's age", default: 40),
field("preference", types.any, doc: "Anything the person likes", default: none)
),
casts: (
(from: "Johnson", with: person => name => person(name, age: 45)),
(from: str, check: name => name.starts-with("Alfred "), with: person => name => person(name, age: 30)),
(from: str, with: person => name => person(name)),
)
)
// Manually invoke typechecking and cast
// Notice how the first succeeding cast is always made
#assert.eq(
types.cast("Johnson", person),
(true, person("Johnson", age: 45))
)
#assert.eq(
types.cast("Alfred Notexistent", person),
(true, person("Alfred Notexistent", age: 30))
)
#assert.eq(
types.cast("abc", person),
(true, person("abc", age: 40))
)
Wait, that sounds a lot like a union
!
That's right: most of the casting generation code is shared with union
! The union
code also contains optimizations for simple types, which we take advantage of here.
The main difference here is that these casts become part of the custom type itself. This means that they will always be there when using this custom type as the type of a field.
However, it's possible to override this behavior: users of the type can disable the casts by wrapping the custom type in the types.exact
combinator.