Awesome
to-typed
Type-guards, casts and converts unknown values into typed values.
Installation
npm install to-typed
Introduction
This package provides 3 interrelated classes: Cast
, Guard
and Convert
.
Cast
The base class of Guard
and Convert
. It is a wrap around a cast
function that takes an unknown value and returns a Maybe
:
cast: (value: unknown) => Maybe<T>
If the cast succeeds, the function returns just
the casted value, otherwise it returns nothing
.
Cast
and its derived classes are designed to make operations chainable in a functional/declarative way:
console.log(Convert
.toArrayWhere(Cast
.asString
.if(str => str.length)
)
.map(arr => arr.join(' '))
.convert([1, null, 'hello', '', 'world', {}, true])
) // "1 hello world true"
Cast factory methods start with the as
prefix, such as asNumber
or asUnknown
.
Guard
A wrap around a guard
function that takes an unknown
value and returns a boolean indicating whether the input value has the expected type:
guard: (input: unknown) => input is T
It implements the cast
method by returning just
the input value if it has the expected type, or nothing
otherwise:
value => guard(value) ? Maybe.just(value) : Maybe.nothing()
Guard factory methods start with the is
prefix, such as isEnum
or isBoolean
.
Convert
A wrap around a convert
function that takes an unknown
value and returns a typed value:
convert: (value: unknown) => T
It implements the cast
method by always returning just
the converted value:
value => Maybe.just(convert(value))
Convert factory methods start with the to
prefix, such as toFinite
or toString
.
Remarks
Note that Guard
and Convert
are complementary subclasses of Cast
in the sense that Guard
cannot provide an alternative to the input value, while Convert
must provide one. The base class Cast
lies in the middle by including both possibilities.
A Guard
can produce a Cast
by calling some value mapping method:
const guard = Guard.is({ value: Guard.isUnknown }); // Guard<{ value: unknown }>
const cast = guard.map(obj => obj.value).asInteger; // Cast<number>
And a Cast
can produce a Convert
by providing a default value:
const convert = cast.if(x => x > 0).else(1); // Convert<number>
console.log(convert.convert({ value: '33.3'})); // 33
Quick Start
import { Guard, Cast, Convert } from "to-typed"
// ---------------- Type guarding ----------------
// Create a `Guard` based on an object, which may include other guards
const guard = Guard.is({
integer: Guard.isInteger,
number: 0,
boolean: false,
tuple: [20, 'default', false] as const,
arrayOfNumbers: Guard.isArrayOf(Guard.isFinite),
even: Guard.isInteger.if(n => n % 2 === 0),
object: {
union: Guard.some(
Guard.isConst(null),
Guard.isString,
Guard.isNumber
),
intersection: Guard.every(
Guard.is({ int: 0 }),
Guard.is({ str: "" })
)
}
})
const valid: unknown = {
integer: 123,
number: 3.14159,
boolean: true,
tuple: [10, 'hello', true],
arrayOfNumbers: [-1, 1, 2.5, Number.MAX_VALUE],
even: 16,
object: {
union: null,
intersection: { int: 100, str: 'good bye' }
}
}
if (guard.guard(valid)) {
// `valid` is now fully typed
console.log(valid.object.intersection.int); // 100
}
// Alternatively, the base class' `cast` method can be used. Since this is
// just a `Guard`, no casting or cloning will actually occur.
const maybe = guard.cast(valid);
if (maybe.hasValue) {
// In this context, `maybe.value` is available and fully typed, and it
// points to the same instance as `valid`.
console.log(maybe.value.object.intersection.int); // 100
}
// Or equivalently...
maybe.read(value => console.log(value.object.intersection.int)); // 100
// ---------------- Type casting / converting ----------------
// Create a `Convert` based on a sample value, from which the default
// values will also be taken if any cast fails.
const converter = Convert.to({
integer: Convert.toInteger(1),
number: 0,
string: '',
boolean: false,
trueIfTruthyInput: Convert.toTruthy(),
tuple: [0, 'default', false] as const,
arrayOfInts: Convert.toArrayOf(Convert.to(0)),
percentage: Convert.toFinite(.5).map(x => Math.round(x * 100) + '%'),
enum: Convert.toEnum('zero', 'one', 'two', 'three'),
object: {
originalAndConverted: Convert.all({
original: Convert.id,
converted: Convert.to('')
}),
strictNumberOrString: Guard.isNumber.or(Convert.to('')),
relaxedNumberOrString: Cast.asNumber.or(Convert.to(''))
}
})
console.log(converter.convert({ excluded: 'exclude-me' }))
// {
// integer: 1,
// number: 0,
// string: '',
// boolean: false,
// trueIfTruthyInput: false,
// tuple: [ 0, 'default', false ],
// arrayOfInts: [],
// percentage: '50%',
// enum: 'zero',
// object: {
// originalAndConverted: { original: undefined, converted: '' },
// strictNumberOrString: '',
// relaxedNumberOrString: ''
// }
// }
console.log(converter.convert({
integer: 2.99,
number: '3.14',
string: 'hello',
boolean: 'true',
trueIfTruthyInput: [],
tuple: ['10', 3.14159, 1, 'exclude-me'],
arrayOfInts: ['10', 20, '30', false, true],
percentage: ['0.33333'],
enum: 'two',
object: {
originalAndConverted: 12345,
strictNumberOrString: '-Infinity',
relaxedNumberOrString: '-Infinity'
}
}))
// {
// integer: 3,
// number: 3.14,
// string: 'hello',
// boolean: true,
// trueIfTruthyInput: true,
// tuple: [ 10, '3.14159', true ],
// arrayOfInts: [ 10, 20, 30, 0, 1 ],
// percentage: '33%',
// enum: 'two',
// object: {
// originalAndConverted: { original: 12345, converted: '12345' },
// strictNumberOrString: '-Infinity',
// relaxedNumberOrString: -Infinity
// }
// }