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
}
}
- Guard:
- A function which lets you narrow down an incoming
unknown
value type, into a defined type.
- A function which lets you narrow down an incoming
- 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.
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
There's also compound guards, which do more complex checks than the primitives:
Higher order guard. This lets the incoming value to also be equal to undefined
as well.
Higher order guard. This lets the incoming value to also be equal to null
or undefined
as well.
Simple guard. This will extract null
and undefined
from the incoming value type.
Higher order guard. This will do the inverse of the incoming guard.
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"
.
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.
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.
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.
Finally there's structure guards, which guard against things like objects/arrays/records.
Simple guard. Passes if the incoming value is an array.
Simple guard. Passes if the incoming value is an object/record, but not if it's an array.
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
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
}
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 unknown
s as the value type.
NOTE: This passes for empty records
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.
These are common use cases for guarding setups, where they are made entirely out of the above guard suite.
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)
);
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"
);
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>;