npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ts-guardian

v1.8.0

Published

Declarative, composable type guards

Downloads

1,486

Readme

MIT license NPM version

ts-guardian

Functional, composable, reliable type guards! 👍

There are already several great type guard packages, but ts-guardian takes it a step further - a functional approach to creating reliable, composable, type-safe type guards.

Type guards?

A type guard is a function that takes any value, and returns a boolean that determines if the value is compatible with a specified type.

Type guards are used to confirm the structure of data. If your app deals with API responses, objects with optional members, or values that are unknown or change regularly, you'll benefit from ts-guardian.

Core principles

  • Concise, human-readable syntax - Type definitions can be complex. ts-guardian's syntax is minimal, declarative, and reads like a sentence.
  • Composable - Complex types are composed of a finite number of basic types. Similarly, type guards can be composed from other type guards due to ts-guardian's functional approach.
  • Full TypeScript support - Guards will type matching values with an auto-generated type, defined by the type-checking process, which can be implicitly cast to a user-defined type while retaining type-safety.
  • Reliable - No false positives, no assumptions, and no TypeScript type assertions or inaccurate type predicates. ts-guardian is 100% type-safe.

Index

Installation

// npm
npm i ts-guardian

// yarn
yarn add ts-guardian

Usage

The is function

import { is } from 'ts-guardian'

Use is to create type guards. The is function takes a parameter that defines a type, and returns a guard for that type:

const isString = is('string') // guard for 'string'

isString('') // returns true
isString(0) // returns false

Basic types

Pass a type as a string to create guards for basic types:

const isBoolean = is('boolean') // guard for 'boolean'
const isNull = is('null') // guard for 'null'

Basic types are the bread and butter of ts-guardian.

Here's the complete set of keys:

| Key | Type | Equivalent type check | | ------------- | ----------- | ------------------------------- | | 'any' | any | true (matches anything) | | 'boolean' | boolean | typeof <value> === 'boolean' | | 'bigint' | bigint | typeof <value> === 'bigint' | | 'function' | Function | typeof <value> === 'function' | | 'null' | null | <value> === null | | 'number' | number | typeof <value> === 'number' | | 'object' | object | typeof <value> === 'object' | | 'string' | string | typeof <value> === 'string' | | 'symbol' | symbol | typeof <value> === 'symbol' | | 'undefined' | undefined | <value> === undefined | | 'unknown' | unknown | true (matches anything) |

When combined with other guards, the any and unknown type guards take precedence. These are useful in complex types where you can specify part of the type as any or unknown, for example, an object member.

Basic type guards will return false for objects created with constructors. For example, is('string')(new String()) returns false. Use isInstanceOf instead.

Union types

Every type guard has an or method which has the same signature as the is function. Use or to create union types:

const isStringOrNumber = is('string').or('number') // guard for 'string | number'

isStringOrNumber('') // returns true
isStringOrNumber(0) // returns true
isStringOrNumber(true) // returns false

Literal types

Pass a number, string, or boolean to the isLiterally function and the orLiterally method to create guards for literal types. You can also pass multiple arguments to create literal union type guards:

import { isLiterally } from 'ts-guardian'

const isCool = isLiterally('cool') // guard for '"cool"'
const is5 = isLiterally(5) // guard for '5'
const isTrue = isLiterally(true) // guard for 'true'
const is1OrTrue = isLiterally(1).orLiterally(true) // guard for '1 | true'
const isCoolOr5OrTrue = isLiterally('cool', 5, true) // guard for '"cool" | 5 | true'

Array types

To check that every element in an array is of a specific type, use the isArrayOf function and the orArrayOf method:

import { is, isArrayOf } from 'ts-guardian'

const isStrArr = isArrayOf('string') // guard for 'string[]'
const isStrOrNumArr = isArrayOf(is('string').or('number')) // guard for '(string | number)[]'
const isStrArrOrNumArr = isArrayOf('string').orArrayOf('number') // guard for 'string[] | number[]'

Note the difference between isArrayOf(is('string').or('number')) which creates a guard for (string | number)[], and isArrayOf('string').orArrayOf('number') which creates a guard for string[] | number[].

Record types

To check that every value in an object is of a specific type, use the isRecordOf function and the orRecordOf method:

import { is, isRecordOf } from 'ts-guardian'

const isStrRecord = isRecordOf('string') // guard for 'Record<PropertyKey, string>'
const isStrOrNumRecord = isRecordOf(is('string').or('number')) // guard for 'Record<PropertyKey, string | number>'
const isStrRecordOrNumRecord = isRecordOf('string').orRecordOf('number') // guard for 'Record<PropertyKey, string> | Record<PropertyKey, number>'

Tuple types

Guards for tuples are defined by passing an array to is:

const isStrNumTuple = is(['string', 'number']) // guard for '[string, number]'

isStrNumTuple(['high']) // returns false
isStrNumTuple(['high', 5]) // returns true

Guards for nested tuples can be defined by passing tuple guards to tuple guards:

const isStrAndNumNumTupleTuple = is(['string', is(['number', 'number'])]) // guard for '['string', [number, number]]'

Object types

The basic type guard for the object type (is('object')) should be used rarely, if at all, due to it matching on null.

Instead, pass an object to is:

const isObject = is({}) // guard for '{}'

isObject({ some: 'prop' }) // returns true
isObject(null) // returns false

To create a guard for an object with specific members, define a guard for each member key:

const hasAge = is({ age: 'number' }) // guard for '{ age: number; }'

hasAge({ name: 'Bob' }) // returns false
hasAge({ name: 'Bob', age: 40 }) // returns true

Intersection types

Every type guard has an and method which has the same signature as the or method. Use and to create intersection types:

const hasXOrY = is({ x: 'any' }).or({ y: 'any' }) // guard for '{ x: any; } | { y: any; }'
const hasXAndY = is({ x: 'any' }).and({ y: 'any' }) // guard for '{ x: any; } & { y: any; }'

hasXOrY({ x: '' }) // returns true
hasXOrY({ y: '' }) // returns true
hasXOrY({ x: '', y: '' }) // returns true
hasXAndY({ x: '' }) // returns false
hasXAndY({ y: '' }) // returns false
hasXAndY({ x: '', y: '' }) // returns true

Instance types

Guards for object instances are defined by passing a constructor object to the isInstanceOf function and the orInstanceOf method:

const isDate = isInstanceOf(Date) // guard for 'Date'

isDate(new Date()) // returns true

const isRegExpOrUndefined = is('undefined').orInstanceOf(RegExp) // guard for 'RegExp | undefined'

isRegExpOrUndefined(/./) // returns true
isRegExpOrUndefined(new RegExp('.')) // returns true
isRegExpOrUndefined(undefined) // returns true

This works with user-defined classes too:

class Person {
  name: string
  constructor(name: string) {
    this.name = name
  }
}

const steve = new Person('Steve')

const isPerson = isInstanceOf(Person) // guard for 'Person'

isPerson(steve) // returns true

User-defined types

Consider the following type and its guard:

type Book = {
  title: string
  author: string
}

const isBook = is({
  title: isString,
  author: isString,
})

If isBook returns true for a value, that value will be the primitive-based type:

{
  title: string
  author: string
}

Ideally, we want to type the value as Book, while avoiding type assertions and user-defined type predicates.

One way is with a parse function that utilizes TypeScript's implicit casting:

const parseBook = (input: any): Book | undefined => {
  return isBook(input) ? input : undefined
}

TypeScript will complain if the type predicate returned from isBook is not compatible with the Book type. This function is type-safe, but defining these functions over and over is a little tedious.

Instead, you can call the parserFor function:

import { parserFor } from 'ts-guardian'

const parseBook = parserFor<Book>(isBook)

The parserFor function takes a guard, and returns a function you can use to parse values.

This function acts in the same way as the previous parseBook function. It takes a value and passes it to the guard. If the guard matches, it returns the value typed as the supplied user-defined type. If the guard does not match, the function returns undefined:

const book = {
  title: 'Odyssey',
  author: 'Homer',
}

const film = {
  title: 'Psycho',
  director: 'Alfred Hitchcock',
}

parseBook(book) // returns book as type 'Book'
parseBook(film) // returns undefined

The parserFor function is also type-safe. TypeScript will complain if you try to create a parser for a user-defined type that isn't compatible with the supplied type guard:

const parseBook = parserFor<Book>(isBook) // Fine

const parseBook = parserFor<Book>(isString) // TypeScript error - type 'string' is not assignable to type 'Book'

Composition

Guards can be composed from existing guards:

const isString = is('string') // guard for 'string'
const isStringOrUndefined = isString.or('undefined') // guard for 'string | undefined'

You can even pass guards into or:

const isStrOrNum = is('string').or('number') // guard for 'string | number'
const isNullOrUndef = is('null').or('undefined') // guard for 'null | undefined'

// guard for 'string | number | null | undefined'
const isStrOrNumOrNullOrUndef = isStrOrNum.or(isNullOrUndef)

Assertion

Use the assertThat function to throw an error if a value does not match against a guard:

import { is, assertThat } from 'ts-guardian'

const value = getSomeUnknownValue()

// Throws an error if type of value is not 'string'
// Error message: Type of 'value' does not match type guard.
assertThat(value, is('string'))

// Otherwise, type of value is 'string'
value.toUpperCase()

You can optionally pass an error message to assertThat:

import { isUser } from '../myTypeGuards/isUser'

assertThat(value, isUser, 'Value is not a user!')

Convenience guards

There are a bunch of simple guards you'll tend to use frequently. ts-guardian exports these as convenience guards to make things easier:

| Guard | Type | Equivalent to | | ----------------------- | -------------------------------------- | -------------------------------- | | isBoolean | boolean | is('boolean') | | isBooleanOrNull | boolean | null | is('boolean').or('null') | | isBooleanOrUndefined | boolean | undefined | is('boolean').or('undefined') | | isBigint | bigint | is('bigint') | | isBigintOrNull | bigint | null | is('bigint').or('null') | | isBigintOrUndefined | bigint | undefined | is('bigint').or('undefined') | | isFunction | Function | is('function') | | isFunctionOrNull | Function | null | is('function').or('null') | | isFunctionOrUndefined | Function | undefined | is('function').or('undefined') | | isNull | null | is('null') | | isNullOrUndefined | null | undefined | is('null').or('undefined') | | isNumber | number | is('number') | | isNumberOrNull | number | null | is('number').or('null') | | isNumberOrUndefined | number | undefined | is('number').or('undefined') | | isString | string | is('string') | | isStringOrNull | string | null | is('string').or('null') | | isStringOrUndefined | string | undefined | is('string').or('undefined') | | isSymbol | symbol | is('symbol') | | isSymbolOrNull | symbol | null | is('symbol').or('null') | | isSymbolOrUndefined | symbol | undefined | is('symbol').or('undefined') | | isUndefined | undefined | is('undefined') |

Reliable type guards

ts-guardian provides type-safe type guards.

Consider the following problem:

Problem

We fetch some data from an API. We expect the data to contain information about the current user. How can we guarantee the data is of the correct User type before we use it in our app?

Here is our User type:

type User = {
  id: number
  name: string
  email?: string
  phone?: {
    primary?: string
    secondary?: string
  }
}

Solution 1 - User-defined type guards 👎

With TypeScript's user-defined type guards, we could write an isUser function to confirm the value is of type User. It would probably look something like this:

// Returns user-defined type guard `input is User`
const isUser = (input: any): input is User => {
  const u = input as User
  return (
    typeof u === 'object' &&
    u !== null &&
    typeof u.id === 'number' &&
    typeof u.name === 'string' &&
    (typeof u.email === 'string' || u.email === undefined) &&
    ((typeof u.phone === 'object' &&
      u.phone !== null &&
      (typeof u.phone.primary === 'string' || u.phone.primary === undefined) &&
      (typeof u.phone.secondary === 'string' || u.phone.secondary === undefined)) ||
      u.phone === undefined)
  )
}

Not pretty, but it works!

Apart from being hard to read and harder to reason about, this function seems to get the job done. We have type safety around the User type. Great!

But what if we did this instead:

const isUser = (input: any): input is User => {
  return typeof input === 'object'
}

Clearly this function is not enough to confirm that input is of type User, but TypeScript doesn't complain at all, because type predicates are effectively type assertions.

By saying to TypeScript "if I return true, consider input to be of type User", we lose type safety, and introduce potential runtime errors into our app. 😫

There is no connection between the result of isUser and the compatibility of the type of input with the User type, other than the assumption that our (obviously error-free) boolean logic accurately defines the type we are asserting. Sounds reliable.

So how can we guarantee that a value is compatible with our user-defined type?

Let's try something else...

Solution 2 - Primitive-based type guards 👍

The solution is that we make no assumptions that the value is a user-defined type.

Instead, we define a primitive-based type of what a User object looks like, and let TypeScript determine whether this primitive-based type is compatible with the User type:

A primitive-based type is a type constructed from only primitive TypeScript types (string, number, undefined, any, etc...).

import { is, isStringOrUndefined, isUndefined } from 'ts-guardian'

// We make no assumptions that the data is a user-defined type
const isUser = is({
  id: 'number',
  name: 'string',
  email: isStringOrUndefined,
  phone: isUndefined.or({
    primary: isStringOrUndefined,
    secondary: isStringOrUndefined,
  }),
})

Not only is this much more readable, but instead of isUser returning the type predicate input is User, it now returns a primitive-based type predicate that gets auto-generated from our type checking, so it's 100% accurate.

In this case, the type predicate looks like:

// Type predicate for our primitive-based type
input is {
    id: number;
    name: string;
    email: string | undefined;
    phone: {
        primary: string | undefined;
        secondary: string | undefined;
    } | undefined;
}

It's now up to TypeScript to tell us if this type is compatible with the User type:

// TypeScript complains if the primitive-based type predicate is not compatible with 'User'
const parseUser = parserFor<User>(isUser)

If the type predicate from isUser is not compatible with the User type, then we get a TypeScript compiler error telling us this. 🎉

Not only that, but the syntax is clean, concise, and readable. Nice! 😎