boenfu / x-value

A medium-neutral runtime type validation library.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

NPM version Repository package.json version Coveralls MIT license

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
# or
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:

type Oops = x.TypeOf<typeof Oops>;
type JSONOops = x.MediumTypeOf<typeof Oops, x.JSONTypes>;

Refine type:

const Email = x.string.refine(value => value.includes('@'));

// Or with refined or nominal type:
const Email = x.string.refine<`${string}@${string}`>(value =>
  value.includes('@'),
);
const Email = x.string.refine<Nominal<'email'>>(value => value.includes('@'));

// Or just nominal type without extra constraints:
const Email = x.string.nominal<'email'>();

Decode from medium:

let value = Oops.decode(x.json, '{"foo":"abc","bar":123}');

Encode to medium:

let json = Oops.encode(x.json, {foo: 'abc', bar: 123});

Transform from medium to medium:

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); // string '6246056b1be8cbf6ca18401f'
ObjectId.encode(rpc, id);     // packed string '"6246056b1be8cbf6ca18401f"'
ObjectId.encode(server, id);  // new ObjectId('6246056b1be8cbf6ca18401f')

let date = new Date('2022-03-31T16:00:00.000Z');

Date.encode(browser, date); // new Date('2022-03-31T16:00:00.000Z')
Date.encode(rpc, date);     // packed string '"2022-03-31T16:00:00.000Z"'
Date.encode(server, date);  // new Date('2022-03-31T16:00:00.000Z')

Or decode packed data of mediums to values:

// All result in '6246056b1be8cbf6ca18401f'
ObjectId.decode(browser, '6246056b1be8cbf6ca18401f');
ObjectId.decode(rpc, '"6246056b1be8cbf6ca18401f"');
ObjectId.decode(server, new ObjectId('6246056b1be8cbf6ca18401f'));

// All result in new Date('2022-03-31T16:00:00.000Z')
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';

// 1. Create a symbol for the new atomic type.
const newAtomicTypeSymbol = Symbol();

// 3. Create the new atomic type with constraint.
const NewAtomic = x.atomic(newAtomicTypeSymbol, value =>
  Buffer.isBuffer(value),
);

declare global {
  namespace XValue {
    interface Types {
      // 2. Define the symbol to value type mapping.
      [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 {
  // 1. Define the unpacked type for decode/encode operation.
  [newAtomicTypeSymbol]: string;
}

const superJSON = x.json.extend<SuperJSONTypes>('Super JSON', {
  codecs: {
    // 2. Define the codec.
    [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');
      },
    },
  },
});

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 {
  // 1. Define the packed type.
  packed: string;
}

const packed = x.medium<PackedTypes>('Packed ', {
  // 2. Define packing methods.
  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.

About

A medium-neutral runtime type validation library.

License:MIT License


Languages

Language:TypeScript 99.5%Language:JavaScript 0.5%