to-typed v0.5.2
to-typed
Type-guards, casts and converts unknown values into typed values.
Installation
npm install to-typedIntroduction
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 TIt 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) => TIt 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'})); // 33Quick 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
// }
// }1 year ago
1 year ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago