io-ts-extra
Version:
Adds pattern matching, optional properties, and several other helpers and types, to io-ts.
153 lines • 6.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.strict = exports.regexp = exports.instanceOf = exports.sparseType = exports.optional = void 0;
const io_ts_1 = require("io-ts");
// eslint-disable-next-line @typescript-eslint/no-duplicate-imports
const t = __importStar(require("io-ts"));
const function_1 = require("fp-ts/lib/function");
const E = __importStar(require("fp-ts/lib/Either"));
/**
* unions the passed-in type with `null` and `undefined`.
* @see sparseType
*/
const optional = (rt, name) => {
const unionType = io_ts_1.union([rt, io_ts_1.nullType, io_ts_1.undefined], name || rt.name + '?');
return Object.assign(unionType, { optional: true });
};
exports.optional = optional;
/**
* Can be used much like `t.type` from io-ts, but any property types wrapped with `optional` from
* this package need not be supplied. Roughly equivalent to using `t.intersection` with `t.type` and `t.partial`.
* @example
* const Person = sparseType({
* name: t.string,
* age: optional(t.number),
* })
*
* // no error - `age` is optional
* const bob: typeof Person._A = { name: 'bob' }
* @param props equivalent to the `props` passed into `t.type`
* @returns a type with `props` field, so the result can be introspected similarly to a type built with
* `t.type` or `t.partial` - which isn't the case if you manually use `t.intersection([t.type({...}), t.partial({...})])`
*/
const sparseType = (props, name) => {
let someOptional = false;
let someRequired = false;
const optionalProps = {};
const requiredProps = {};
for (const key of Object.keys(props)) {
const val = props[key];
if (val.optional) {
someOptional = true;
optionalProps[key] = val;
}
else {
someRequired = true;
requiredProps[key] = val;
}
}
const computedName = name || getInterfaceTypeName(props);
if (someOptional && someRequired) {
return Object.assign(io_ts_1.intersection([io_ts_1.type(requiredProps), io_ts_1.partial(optionalProps)], computedName), { props });
}
else if (someOptional) {
return io_ts_1.partial(props, computedName);
}
return io_ts_1.type(props, computedName);
};
exports.sparseType = sparseType;
const getNameFromProps = (props) => Object.keys(props)
.map(k => `${k}: ${props[k].name}`)
.join(', ');
const getInterfaceTypeName = (props) => {
return `{ ${getNameFromProps(props)} }`;
};
/**
* Validates that a value is an instance of a class using the `instanceof` operator
* @example
* const DateType = instanceOf(Date)
* DateType.is(new Date()) // right(Date(...))
* DateType.is('abc') // left(...)
*/
const instanceOf = (cns) => new t.Type(`InstanceOf<${cns.name || 'anonymous'}>`, (v) => v instanceof cns, (s, c) => (s instanceof cns ? t.success(s) : t.failure(s, c)), t.identity);
exports.instanceOf = instanceOf;
/**
* A type which validates its input as a string, then decodes with `String.prototype.match`,
* succeeding with the RegExpMatchArray result if a match is found, and failing if no match is found.
*
* @example
* const AllCaps = regexp(/\b([A-Z]+)\b/)
* AllCaps.decode('HELLO') // right([ 'HELLO', index: 0, input: 'HELLO' ])
* AllCaps.decode('hello') // left(...)
* AllCaps.decode(123) // left(...)
*/
exports.regexp = (() => {
const RegExpMatchArrayStructure = t.intersection([
t.array(t.string),
t.type({
index: t.number,
input: t.string,
}),
]);
return (v) => {
const RegExpMatchArrayDecoder = new t.Type(`RegExp<${v.source}>`, RegExpMatchArrayStructure.is, (s, c) => {
// note: this implementation used to be much simpler:
// return RegExpMatchArrayStructure.validate(s.match(v), c)
// but a change to io-ts means that `t.type` won't validate an array, even if
// the array does have the properties required by the t.type.
const [array, structure] = RegExpMatchArrayStructure.types;
const match = s.match(v);
return function_1.pipe(match, E.fromNullable(t.failure(s, c, `No match found for regexp ${v}`)), E.mapLeft(e => e.left), E.chain(match => array.validate(match, c)), E.map((match) => ({ index: match.index, input: match.input })), E.chain(struct => structure.validate(struct, c)), E.chain(() => t.success(match)));
}, val => val.input);
return t.string.pipe(RegExpMatchArrayDecoder);
};
})();
/**
* Like `t.type`, but fails when any properties not specified in `props` are defined.
*
* @example
* const Person = strict({name: t.string, age: t.number})
*
* expectRight(Person.decode({name: 'Alice', age: 30}))
* expectLeft(Person.decode({name: 'Bob', age: 30, unexpectedProp: 'abc'}))
* expectRight(Person.decode({name: 'Bob', age: 30, unexpectedProp: undefined}))
*
* @param props dictionary of properties, same as the input to `t.type`
* @param name optional type name
*
* @description
* note:
* - additional properties explicitly set to `undefined` _are_ permitted.
* - internally, `sparseType` is used, so optional properties are supported.
*/
const strict = (props, name) => {
const codec = exports.sparseType(props);
return new t.Type(name || `Strict<${codec.name}`, (val) => codec.is(val) && Object.keys(val).every(k => k in props), (val, ctx) => {
if (typeof val !== 'object' || !val) {
return codec.validate(val, ctx);
}
const stricterProps = Object.keys(val).reduce((acc, next) => (Object.assign(Object.assign({}, acc), { [next]: props[next] || t.undefined })), props);
return exports.sparseType(stricterProps).validate(val, ctx);
}, codec.encode);
};
exports.strict = strict;
//# sourceMappingURL=combinators.js.map