deep-guards
TypeScript icon, indicating that this package has built-in type declarations

1.1.0 • Public • Published

Deep-Guards

This solves the cases where you have a nested JSON data structure, and are wanting to guard the whole structure in one go, rather than doing shallow guards.

I was also surprised that there's not too many options for guarding things with type narrowing in the case of discriminated unions. This library also solves that!

I have also oriented this library towards making the type tooltips as descriptive as possible.

Example use case:

const carGuard = isObjectOf({
  type: isExact("car"),
  wheels: isExact(4),
  owner: isString,
  passengers: isArrayOf(
    isObjectOf({
      name: isString,
    })
  ),
});

const bikeGuard = isObjectOf({
  type: isExact("bike"),
  wheels: isExact(2),
  owner: isString,
  storage: isOptional(isArrayOf(isString)),
});

const vehicleGuard = isUnionOf(carGuard, bikeGuard);

const value: unknown = { ... };

if (vehicleGuard(value)) {
  console.log(value.wheels);

  if (value.type === "car") {
    // value is a Car
  } else {
    // value is a Bike
  }
}

Contents

  1. Primitives
  2. Compound
    1. isOptional
    2. isNullable
    3. isNonNullable
    4. isNot
    5. isOneOf
    6. isUnionOf
    7. isIntersectionOf
    8. isExact
  3. Structures
    1. isAnyArray
    2. isAnyRecord
    3. isArrayOf
    4. isTupleOf
    5. isRecordOf
    6. isObjectOf
  4. Macros
    1. isDiscriminatedObjectOf
  5. guardOrThrow
  6. TypeFromGuard

Terminology

  • Guard:
    • A function which lets you narrow down an incoming unknown value type, into a defined type.
  • Simple guard:
    • A variable which is set to a Guard
  • Higher order guard:
    • This is a function which takes in a guard function as a parameter, and then will produce a new guard function as a result.
  • Incoming value:
    • The value being guarded against.

Primitives

There's all the usual suspects for primitives. They are all simple guards.

The full list of available primitive guards is below:

  • isUnknown
  • isNull
  • isUndefined
  • isNumber
  • isInteger
  • isString
  • isSymbol
  • isBoolean
  • isFunction

Compound

There's also compound guards, which do more complex checks than the primitives:

isOptional

Higher order guard. This lets the incoming value to also be equal to undefined as well.

isNullable

Higher order guard. This lets the incoming value to also be equal to null or undefined as well.

isNonNullable

Simple guard. This will extract null and undefined from the incoming value type.

isNot

Higher order guard. This will do the inverse of the incoming guard.

isOneOf

Has signature:

function isOneOf<
  const T extends (string | number | boolean | symbol | null | undefined)[]
>(...values: T): Guard<(typeof values)[number]>;

It's very useful for enumerations, where you only have a few specific values, e.g. a status enum with a guard: isOneOf("active", "inactive", "away"). When using the enum, it will then be narrowed down to those specific values, so in the case of the example, a value will be narrowed down to the type: "active" | "inactive" | "away".

isUnionOf

Higher order guard. This takes in any amount of guards as arguments, and then produces a guard which does a union over all the incoming guards. This means that if any one of the guards passes for the incoming value, then this will pass.

isIntersectionOf

Higher order guard. This takes in any amount of guards as arguments, and then produces a guard which does an intersection over all the incoming guards. This means that if every one of the guards passes for the incoming value, then this will pass.

isExact

Has signature:

function isExact<const T>(expected: T, deep: boolean = true): Guard<T>;

This will pass if the incoming value exactly matches the expected parameter, optionally computing a deep equality.

Structures

Finally there's structure guards, which guard against things like objects/arrays/records.

isAnyArray

Simple guard. Passes if the incoming value is an array.

isAnyRecord

Simple guard. Passes if the incoming value is an object/record, but not if it's an array.

isArrayOf

Higher order guard. This will pass if the incoming value is an array which contains elements which are of the type of the passed in guard function.

NOTE: This passes for empty arrays

isTupleOf

Higher order guard. This takes in any number of guards, and then checks that the incoming value is an array of the same size, with the guards guarding the items in the same order as they appear.

For example:

const myTupleGuard = isTupleOf(isNumber, isString, isBoolean);
const value: unknown = [1, "foo", true];

if (myTupleGuard(value)) {
  // value passes
}

isRecordOf

Higher order guard. It has two guard parameters, where the first is the key guard, and then the second is a value guard which is optional. If you don't pass in a value guard, the returned guard function has unknowns as the value type.

NOTE: This passes for empty records

isObjectOf

This is a function which takes in a structured object, containing keys of type string | number | symbol, and then values which are guard functions.

This also takes in a second boolean parameter for if the guard should check that the keys exactly match the structured guard object's keys. It then doesn't let any "leaky" objects through, which have extra keys. This defaults to false.

As seen in the example at the start of this readme, you can do all sorts of complex nesting, as this produces a guard in the end.

NOTE: This throws an error if you give it an empty object.
It will also accept an object which contains keys which are not specified.

Macros

These are common use cases for guarding setups, where they are made entirely out of the above guard suite.

isDiscriminatedObjectOf

This takes in a string literal for the discriminated value, and an isObjectOf guard, and then an optional key specifying which key is for the discriminated union. This then combines the object with the discriminator, where the returned guard has the signature: Guard<{ [key]: T } & O>.

This is good for use cases where you don't have a discriminator on an individual type, but then do have it on the union type. For example:

interface Car {
  wheels: 4;
  owner: string;
  passengers: {
    name: string;
  }[];
}

interface Bike {
  wheels: 2;
  owner: string;
  storage?: string[];
}

type Vehicle = ({ type: "car" } & Car) | ({ type: "bike" } & Bike);

// Can then be represented like so in guards:

const carGuard = isObjectOf({
  wheels: isExact(4),
  owner: isString,
  passengers: isArrayOf(
    isObjectOf({
      name: isString,
    })
  ),
});

const bikeGuard = isObjectOf({
  wheels: isExact(2),
  owner: isString,
  storage: isOptional(isArrayOf(isString)),
});

const vehicleGuard = isUnionOf(
  isDiscriminatedObjectOf("car", carGuard),
  isDiscriminatedObjectOf("bike", bikeGuard)
);

guardOrThrow

This package also includes a guardOrThrow method which when given an incoming value, a guard, and an optional hint message, will return a narrowed version of the value, or throw a GuardError containing that hint message.

You can then do things like:

const cars = guardOrThrow(
  JSON.parse(readFileSync("cars.json").toString()),
  isArrayOf(isCar),
  "Invalid car format"
);

TypeFromGuard

This is a helper type, which will extract the type from a guard function to then let you use the type for other purposes.

An example use case for this is:

const carGuard = isObjectOf({
  type: isExact("car"),
  wheels: isExact(4),
  owner: isString,
  passengers: isArrayOf(
    isObjectOf({
      name: isString,
    })
  ),
});

type Car = TypeFromGuard<typeof carGuard>;

Readme

Keywords

none

Package Sidebar

Install

npm i deep-guards

Weekly Downloads

88

Version

1.1.0

License

Apache-2.0

Unpacked Size

3.39 MB

Total Files

52

Last publish

Collaborators

  • eniallator