UNPKG

to-typed

Version:

Type-guards, casts and converts unknowns into typed values

224 lines (180 loc) 6.72 kB
# 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`: ```typescript cast: (value: unknown) => Maybe<T> ``` If the cast succeeds, the function returns [`just`](https://github.com/jsoldi/to-typed/blob/3304df454a9721f6b3b65b90c1ff4a0953537d36/src/lib/maybe.ts#L4) the casted value, otherwise it returns [`nothing`](https://github.com/jsoldi/to-typed/blob/3304df454a9721f6b3b65b90c1ff4a0953537d36/src/lib/maybe.ts#L9). `Cast` and its derived classes are designed to make operations chainable in a functional/declarative way: ```typescript 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: ```typescript 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: ```typescript 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: ```typescript convert: (value: unknown) => T ``` It implements the `cast` method by always returning `just` the converted value: ```typescript 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: ```typescript 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: ```typescript const convert = cast.if(x => x > 0).else(1); // Convert<number> console.log(convert.convert({ value: '33.3'})); // 33 ``` ## Quick Start   ```typescript 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 // } // } ```