A lightweight type checking module for Typescript with typegaurds for common use cases which can be extended to handle niche cases
This is a small, lightweight type checking library that also satisfies the TS compiler with typegaurds. It can be extended to have composite typegaurds, to check if something is an array of numbers for example, or an array of array of numbers (or, indeed, whatever!) at the type level.
The inspiration for this came from viewing this tweet from Todd Motto. It piqued my interest and I decided to look into it deeper and convert it to Typescript. Unfortunately, as it is, that implementation is not typesafe at the typescript level (which is to be expected!). However, what this means is that if you were to use that code to check for example isArray
on const variable: unknown = []
, and then try to do variable.map
typescript would still complain even inside an if check using the isArray
you've written. This library combines the two approaches - you get the runtime type checking and the compile type safety/IDE autocomplete from typescript. I then went a step further and for the case of arrays/objects, made it so you could construct composite type gaurds, to check if something is an array of numbers for example, or an array of array of numbers, or an array of objects of signature xyz (or, indeed, whatever you can come up with/be bothered to write a custom typegaurd callback for!).
import typeKeeper from './typekeeper';
const myArray: unknown = [1, 2, 3];
// As expected, TS will complain as myArray is 'unknown'.
myArray.map(x => x * 2)
if (typeKeeper.isArray(myArray)) {
// TypeScript knows that myArray is an array here, but doesn't know the type of x
// so it's 'any', but TS won't complain
console.log(myArray.map(x => x * 2));
}
if (typeKeeper.isArray(myArray, typeKeeper.isNumber)) {
// TypeScript knows that myArray is an array of numbers here as we've passed a second check to isArray which ensures
// every element in the array passes the type predicate for isNumber
console.log(myArray.map(x => x * 2));
}
const myArrayOfArrays: unknown = [[1], [2], [3]];
// Custom nested type predicate callback we can pass through to isArray
const isNumberArray = (arg: any): arg is number[] => typeKeeper.isArray(arg, typeKeeper.isNumber);
if (typeKeeper.isArray(myArrayOfArrays, isNumberArray)) {
// TypeScript knows that myArray is an array of array of numbers here
// It knows 'x' is of type array, with each element being a number, and so knows y is a number
console.log(myArrayOfArrays.map((x) => x.map(y => y * 2)));
}
npm i typekeeper
yarn add typekeeper
import { typeKeeper } from 'typekeeper';
import typeKeeper from './typekeeper';
const myArray: unknown = [1, 2, 3];
// As expected, TS will complain as myArray is 'unknown'.
myArray.map(x => x * 2)
if (typeKeeper.isArray(myArray)) {
// TypeScript knows that myArray is an array here, but doesn't know the type of x
// so it's 'any', but this won't complain
console.log(myArray.map(x => x * 2));
}
if (typeKeeper.isArray(myArray, typeKeeper.isNumber)) {
// TypeScript knows that myArray is an array of numbers here as we've passed a second check to isArray
console.log(myArray.map(x => x * 2));
}
const myArrayOfArrays: unknown = [[1], [2], [3]];
// Custom nested type predicate callback we can pass through to isArray
const isNumberArray = (arg: any): arg is number[] => typeKeeper.isArray(arg, typeKeeper.isNumber);
if (typeKeeper.isArray(myArrayOfArrays, isNumberArray)) {
// TypeScript knows that myArray is an array of array of numbers here
// It knows 'x' is of type array, with each element being a number, and so knows y is a number
console.log(myArrayOfArrays.map((x) => x.map(y => y * 2)));
}
const myArray: unknown = [1, 2, 3];
if (typeKeeper.isArray<number>(myArray)) {
// TypeScript knows that myArray is an array here and we've told it it's of type number, but
// we don't have a 'true' check for number, we've effectively just done an 'as number' cast
console.log(myArray.map(x => x * 2));
}
const myString: unknown = 'hello';
if (typeKeeper.isString(myString)) {
// TypeScript knows that myString is a string here so this is fine
console.log(myString.toUpperCase());
}
const myObject: unknown = { a: 1, b: 2 };
if (typeKeeper.isObject(myObject)) {
// TypeScript knows that myObject is an object here but doesn't know the signature, it's essential Record<string, any>
console.log(myObject.a, myObject.b);
}
// Contrived object check
function customObjectCheckExample(x: any): x is { c: number } { return typeKeeper.isNumber(x.c) }
const myObject: unknown = { a: 1, b: 2 };
if (typeKeeper.isObject(myObject, customObjectCheckExample)) {
// TypeScript knows that myObject is an object here with a property 'c' that is a number
// TS will complain as a doesn't exist!
console.log(myObject.a, myObject.c);
}
- Create a branch from the
develop
branch and submit a Pull Request (PR)- Explain what the PR fixes or improves
- Use sensible commit messages which follow the Conventional Commits specification.
- Use a sensible number of commit messages
Our versioning uses SemVer and our commits follow the Conventional Commits specification.
- Make changes
- Commit those changes
- Pull all the tags
- Run
npm version [patch|minor|major]
- Stage the
CHANGELOG.md
,package-lock.json
andpackage.json
changes - Commit those changes with
git commit -m "chore: bumped version to $version"
- Push your changes with
git push
and push the tag withgit push origin $tagname
where$tagname
will bev$version
e.g.v1.0.4
-
Clone the repository
-
Install dependencies:
npm ci
-
Test:
npm test
See CHANGELOG.md