README
vee-type-safe
This is a simple TypeScript type checking utility library.
Requires Typescript version >= 3.2
.
View detailed API documentation generated by TypeDoc
Quick API glance
mismatch(suspect: unknown, typeDescr: TypeDescription)
Returns null
or a MismatchInfo
object that stores information
about type incompatability with the given
TypeDescription
, e.g. why and where suspect
's invalid property is.
This is a powerful tool to generate useful error messages while validating value shape type.
Note: this function doesn't allow suspect
to have properties not listed in typeDescr
which differentiates it from duckMismatch()
(see bellow).
import * as Vts from 'vee-type-safe';
import { Model } from '@models/model';
const untrustedJson: unknown = /* ... */;
const ExpectedJsonTD: Vts.TypeDescription = /* this is actually a generic type (advanced topic) */;
const dbDocument: Model = /* Some object */;
const mismatchInfo = Vts.mismatch(untrustedJson, ExpectedJsonTD);
if (mismatchInfo != null) {
console.log(
mismatchInfo.path,
mismatchInfo.actualValue,
mismatchInfo.expectedTd
);
// logs human readable path to invalid property
console.log(mismatchInfo.pathString());
// mismatchInfo.toErrorString() generates human readable error message
throw new Vts.TypeMismatchError(mismatchInfo);
}
// now you may safely assign untrustedJson to dbDocument:
Object.assign(dbDocument, untrustedJson);
duckMismatch(suspect, typeDescr)
Works the same way as mismatch(suspect, typeDescr)
but allows suspect
object with excess properties to pass the match.
import * as Vts from 'vee-type-safe';
Vts.duckMismatch(
{ name: 'Ihor', somePropertyIDontCareAbout: 42 },
{ name: 'string' }
); // returns null as suspect is allowed to have excess properties
const untrustedJson = {
client: 'John Doe',
walletNumber: null,
};
const ExpectedJsonTD = Vts.td({ // this noop call is needed to preserve unit types
client: 'string',
walletNumber: /\d{16}/ // implies a string of the given format
});
// Here we map the given type description shape to the type that it describes statically
type ExpectedJson = Vts.TypeDescriptionTarget<typeof ExpectedJsonTD>;
/*
ExpectedJson === {
client: string;
walletNumber: string;
}
*/
const mismatchInfo = Vts.duckMismatch(untrustedJson, ExpectedJsonTD);
if (mismatchInfo != null) {
throw new Vts.TypeMismatchError(mismatchInfo);
}
// ^~~~ Vts.ensureDuckMatch() does the same
const trustedJson = untrustedJson as ExpectedJson;
// process client
What is TypeDescription?
Type description is a simple JavaScript object with values of TypeDescription
type or basic typename string ('string'
,
'number'
, 'function'
...) or Set<TypeDescription>
or TypeDescription[]
or RegExp
or your
custom TypePredicate
function. TypeDescription
is actually a conditional (dependent on type argument) union type of all of these.
Here is an example of how you may describe your type.
import * as Vts from 'vee-type-safe';
Vts.conforms(
{
prop: 'lala',
tel: '8800-555-35-35'
prop2: true,
obj: {
obj: [23, false]
},
someIDontCareProperty: null // excess properties are ok for confroms()
},
{
prop: 'string',
tel: /\d{4}-\d{3}-\d{2}-\d{2}/, // claims a string of given format
prop2: 'boolean',
obj: {
obj: ['number', 'boolean'] // claims a fixed length tuple
}
}); // true
Vts.conforms(
{
arr: ['array', null, 'of any type', 8888 ],
strArr: ['Pinkie', 'Promise', 'some', 'strings'],
oneOf: 2,
custom: 43
},
{
arr: [], // claims an array of any type
strArr: ['string'], // claims an array of any length
oneOf: new Set(['boolean', 'number']),// claims to be one of these types
custom: isOddNumber // custom type predicate function
}); // true
function isOddNumber(suspect: unknown): suspect is number {
return typeof suspect === 'number' && suspect % 2;
}
const HumanTD = Vts.td({ // noop function that preserves unit types
name: 'string',
id: 'number'
});
// generate static TypeScript type:
type Human = Vts.TypeDescriptionTarget<typeof HumanTD>;
// type Human === {
// name: string;
// id: number;
// }
function tryUseHuman(maybeHuman: unknown) {
if (conforms(maybeHuman, HumanTD)) {
// maybeHuman is of type that is assignable to Human here
// it is inferred to be Vts.TypeDescriptionTarget<typeof HumanTD> exactly
maybeHuman.name;
maybeHuman.id;
}
}
Here is an actual algorithm how conforms()
function interprets TypeDescription
.
- If it is a basic JavaScript typename string (should satisfy typeof operator
domain definition), then function returns
typeof suspect === typeDescr
. - If it is a
RegExp
, then returnstypeof suspect === 'string' && typeDescr.test(suspect)
. - If it is a
Set<TypeDescription>
, returnstrue
if suspect conforms to at least one of the given TDs inSet
. - If it is an
Array<TypeDescription>
and it consists of one item, returnstrue
ifsuspect
isArray
and each of its items conforms to the given TD attypeDescr[0]
. - If it is an
Array<TypeDescription>
and it consists of more than one item, returnstrue
if suspect isArray
andsuspect.length === typeDescr.length
and each correspondingsuspect[i]
conforms totypeDescr[i]
type description. - If it is an empty
Array
, returnstrue
ifsuspect
isArray
of any type. - If it is an object, returns
true
ifsuspect
is also an object and eachtypeDescr[key]
is a TD forsuspect[key]
. Excess properties insuspect
do not matter forconforms()
function, but matter forexactlyConforms()
andmismatch()
functions. - If it is a
TypePredicate
(i.e.(suspect: unknown) => boolean
), then returnstypeDescr(suspect)
.
Predefined TypeDescriptions
There are factory functions that return TypeDescription
s (those are often TypePredicate
s) or already defined TypePredicates
, that you should use as type descriptions when calling mismatch/duckMismatch/conforms/exactlyConforms(suspect, typeDescr)
.
TypePredicate
is a function of type:
(suspect: unknown) => boolean
If you specify a generic argument TTarget
it becomes a true TypeScript type predicate, so that you will be able to get described type from it when using Vts.TypeDescriptionTarget
:
(suspect: unknown) => suspect is TTarget
isNumberWithinRange(min, max)
Returns a predicate that returns true if its argument is a number within the range [min
, max
] or [max
, min
] if min > max
.
import * as Vts from 'vee-type-safe';
Vts.conforms(
{
num: 32
},
{
num: Vts.isNumberWithinRange(0, 5)
}); // false
isIntegerWithinRange(min, max)
The same as isNumberWithinRange(min, max)
, but its returned predicate returns false if forwarded argument is not an integer.
optional(typeDescr: TypeDescription)
Retuns Set(['undefined', typeDescr]))
import * as Vts from 'vee-type-safe';
Vts.conforms(
{
prop: 'str'
},{
prop: Vts.optional('number')
})
// return false because the property is not undefined,
// but doesn't conform to 'number' type
Vts.conforms(
{
prop: -23
},{
prop: Vts.optional(Vts.isNegativeInteger)
});
// returns true because the property is not undefined
// and conforms to isNegativeInteger restriction
Vts.conforms(
{
},{
prop: Vts.optional(Vts.isNegativeInteger)
});
// returns true because property 'prop' may be absent
Self explanatory functions
All these functions take unknown
type argument and return suspect is number
, which is useful as a type guard or when using as a type description.
isInteger(suspect)
isPositiveInteger(suspect)
isNegativeInteger(suspect)
isPositiveNumber(suspect)
isNegativeNumber(suspect)
isZeroOrPositiveInteger(suspect)
isZeroOrNegativeInteger(suspect)
isZeroOrPositiveNumber(suspect)
isZeroOrNegativeNumber(suspect)
- ...
import * as Vts from 'vee-type-safe';
Vts.conforms(
{
id: 2,
volume: 22.5
},
{
id: Vts.isPositiveInteger,
money: Vts.isZeroOrPositiveNumber
}); // true
isOneOf<T>(possibleValues: T[])
Returns a predicate that accepts a suspect of any
type and matches it to
one of the provided possible values by
possibleValues.includes(suspect)
. Don't confuse it with new Set(possibleValues)
when forwarding as a type description to conforms()
function, because possibleValues
are not TDs, but values to match with.
import * as Vts from 'vee-type-safe';
Vts.conforms(2, Vts.isOneOf([0, 1, 2, 3])); // true
Vts.conforms(2, new Set([0, 1, 2, 3])); // compile error
// Set<numbers> is not a Set<TypeDescritpion>
Convenient type definitions
interface BasicObject<T>
A shorthand for { [key: string]: T; }
type.
type PrimitiveType
A union of all primitive types (null
is treated as a primitive type).
type BasicTypeName
A union type of string literals which are in typeof
operator domain definition ('string' | 'boolean' | 'object' ...
).
vee-type-safe/express (BETA)
This is a library for ExpressJS routing middleware functions.
ensureTypeMatch(getRequestProperty, typeDescr, makeError?)
Returns express.Handler
that exactly matches the value returned by getRequestProperty(req)
to typeDescr
and if it fails, calls next(makeError(failedTypeInfo))
.
Thus you can be sure that the property of express.Request
object was type checked before using it in your middleware.
Does type matching via core library mismatch()
function.
getRequestProperty: (req: express.Request) => unknown
- this function must return a suspect to match totypeDescr
, based on the givenreq
argument.typeDescr
- type description that the value returned bygetRequestProperty(req)
will be checked to match tomakeError?: (failInfo: MismatchInfo) => unknown
- it is an optional function which makes a custom error to forward tonext()
, by default this function retunsBadTypeStatusError
BadTypeStatusError
is an instance of TypeMismatchError
that has a status: number
property, which is http BAD_REQUEST by default.
import * as express from 'express';
import * as VtsEx from 'vee-type-safe/express'
import * as Vts from 'vee-type-safe';
const router = express.Router();
interface MessagesPostRequest {
filters: string[];
limit: number;
}
router.post('api/v1/messages',
VtsEx.matchType(
VtsEx.ReqBody, // or req => req.body (your custom obtaining logic here)
{
filters: ['string'],
limit: Vts.isPositiveInteger
},
mmInfo => new MyCustomError(mmInfo.path, mmInfo.actualValue)
),
// replaces standard express.Request.body type with MessagesPostRequest
(req: VtsEx.ReqBody<MessagesPostRequest>, res, next) => {
/* your middleware, where you can trust to req.body */
// req.body has MessagesPostRequest type here
const filters = req.body.filters.join();
// ...
}
);
There is a list of handy functions to specify as getRequestProperty
argument:
ReqBody(req) => req.body
ReqParams(req) => req.params
ReqQuery(req) => req.query
ReqCookies(req) => req.cookies
ReqHeaders(req) => req.headers
import * as VtsEx from 'vee-type-safe/express';
/* ... */
router.get('api/v1/users/',
VtsEx.matchType(VtsEx.ReqQuery, { title: 'string' }),
(req: VtsEx.ReqQuery<{title: string}>, res, next) => {
const title: string = req.query.title; // now you are sure
/* ... */
}
);