X-Value
X-Value (X stands for "cross") is a medium-somewhat-neutral runtime type validation library.
Comparing to alternatives like io-ts and Zod, X-Value uses medium/value concept and allows values to be decoded from and encoded to different mediums.
Installation
yarn add x-value
npm install x-value
Usages
Defining types with X-Value is similar to io-ts/Zod.
import * as x from 'x-value';
const Oops = x.object({
foo: x.string,
bar: x.number.optional(),
});
const Rock = x.record(x.string, x.number);
const Aha = x.array(Oops);
const Tick = x.tuple(x.string, x.number);
const Um = x.union(Oops, x.boolean);
const I = x.intersection(
Oops,
x.object({
yoha: x.boolean,
}),
);
interface R {
type: 'recursive';
child: R;
}
const R = x.recursive<R>(R =>
x.object({
type: x.literal('recursive'),
child: R,
}),
);
Get static type of type object:
declare global {
namespace XValue {
interface Using extends x.UsingJSONMedium {}
}
}
type Oops = x.TypeOf<typeof Oops>;
type JSONOops = x.MediumTypeOf<typeof Oops, 'json'>;
Refine type:
const Email = x.string.refine(value => value.includes('@'));
const Email = x.string.refine<never, `${string}@${string}`>(value =>
value.includes('@'),
);
const Email = x.string.refine<'email'>(value => value.includes('@'));
const Email = x.string.nominal<'email'>();
Decode from medium:
declare global {
namespace XValue {
interface Using extends x.UsingJSONMedium {}
}
}
let value = Oops.decode(x.json, '{"foo":"abc","bar":123}');
Encode to medium:
declare global {
namespace XValue {
interface Using extends x.UsingJSONMedium {}
}
}
let json = Oops.encode(x.json, {foo: 'abc', bar: 123});
Transform from medium to medium:
declare global {
namespace XValue {
interface Using extends x.UsingJSONMedium, x.UsingQueryStringMedium {}
}
}
let json = Oops.transform(x.queryString, x.json, 'foo=abc&bar=123');
Type is
guard:
if (Oops.is(value)) {
}
Type satisfies
assertion (will throw if does not satisfy):
let oops = Oops.satisfies(value);
Diagnose for type issues:
let issues = Oops.diagnose(value);
Mediums and Values
Mediums are what's used to store values: JSON strings, query strings, buffers etc.
For example, a string "2022-03-31T16:00:00.000Z"
in JSON medium with type Date
represents value new Date('2022-03-31T16:00:00.000Z')
.
Assuming we have 3 mediums: browser
, server
, rpc
; and 2 types: ObjectId
, Date
. Their types in mediums and value are listed below.
Type\Medium | Browser | RPC | Server | Value |
---|
ObjectId | string | packed as string | ObjectId | string |
Date | Date | packed as string | Date | Date |
We can encode values to mediums:
let id = '6246056b1be8cbf6ca18401f';
ObjectId.encode(browser, id);
ObjectId.encode(rpc, id);
ObjectId.encode(server, id);
let date = new Date('2022-03-31T16:00:00.000Z');
Date.encode(browser, date);
Date.encode(rpc, date);
Date.encode(server, date);
Or decode packed data of mediums to values:
ObjectId.decode(browser, '6246056b1be8cbf6ca18401f');
ObjectId.decode(rpc, '"6246056b1be8cbf6ca18401f"');
ObjectId.decode(server, new ObjectId('6246056b1be8cbf6ca18401f'));
Date.decode(browser, new Date('2022-03-31T16:00:00.000Z'));
Date.decode(rpc, '"2022-03-31T16:00:00.000Z"');
Date.decode(server, new Date('2022-03-31T16:00:00.000Z'));
Ideally there's no need to have "value" as a separate concept because it's essentially "ECMAScript runtime medium". But to make decode/encode easier among different mediums, "value" is promoted as an interchangeable medium.
New Atomic Type
Before we can add medium support for a new type of atomic value, we need to add new atomic value. It is quite easy to do so:
import * as x from 'x-value';
const newAtomicTypeSymbol = Symbol();
const NewAtomic = x.atomic(newAtomicTypeSymbol, value =>
Buffer.isBuffer(value),
);
declare global {
namespace XValue {
interface Types {
[newAtomicTypeSymbol]: Buffer;
}
}
}
New Medium
After creating the new atomic type, we need to create/extend a new medium that supports this type:
interface SuperJSONTypes extends x.JSONTypes {
[newAtomicTypeSymbol]: string;
}
interface UsingSuperJSONMedium {
'super-json': SuperJSONTypes;
}
const superJSON = x.json.extend<UsingSuperJSONMedium>('super-json', {
codecs: {
[newAtomicTypeSymbol]: {
decode(value) {
if (typeof value !== 'string') {
throw new TypeError(
`Expected hex string, getting ${Object.prototype.toString.call(
value,
)}`,
);
}
return Buffer.from(value, 'hex');
},
encode(value) {
return value.toString('hex');
},
},
},
});
To use this medium:
declare global {
namespace XValue {
interface Using extends x.UsingSuperJSONMedium {}
}
}
Medium Packing
When decode()
from a medium, X-Value unpacks data for a structured input (e.g., JSON.parse()
). It packs the data again on encode()
(e.g., JSON.stringify()
).
For medium that requires packing:
interface PackedTypes {
packed: string;
}
const packed = x.medium<PackedTypes>('Packed ', {
packing: {
pack(data) {
return JSON.stringify(data);
},
unpack(json) {
return JSON.parse(json);
},
},
});
The superJSON
medium is actually a packed medium. However, the related definitions are inherited from x.JSONTypes
.
License
MIT License.