ts-guardian
v1.11.0
Published
Declarative, composable type guards
Maintainers
Readme
ts-guardian
Runtime type guards. Composable, TypeScript-like syntax. 100% type-safe.
Full TypeScript support (TypeScript not required).
Type guards?
Type guards let you check whether a value matches a type at runtime.
ts-guardian makes them composable, readable, and type-safe — no more verbose checks or unsafe assertions.
If you're working with API responses, optional object members, or unknown values, ts-guardian will help you out.
Installation
npm install ts-guardianQuick Start
Import the is function and create a type-guard, such as isUser. Use that type-guard to confirm objects match the type:
import { is } from 'ts-guardian'
const isUser = is({
id: 'number',
name: 'string',
email: 'string?',
teamIds: 'number[]',
})
isUser({ id: 1, name: 'John', teamIds: [2, 3] }) // trueCore principles
- Minimal and declarative syntax improves readability.
- Composable type guards mimick the way types are constructed.
- 100% type-safety means no assumptions, type assertions, or inaccurate type predicates.
API
isfunction- Basic types
- Union types
- Intersection types
- Literal types
- Array types
- Object types
- Tuple types
- Instance types
- Optional and nullable types
- Parsing to user-defined types
- Composition
- Throwing
is function
import { is } from 'ts-guardian'The main tool to create type guards. The is function takes a parameter that defines a type, and returns a guard for that type:
const isNumber = is('number') // guard for 'number'
isNumber(0) // true
isNumber('') // falseBasic types
Pass a type string to create guards for basic types:
const isBoolean = is('boolean') // guard for 'boolean'
const isNull = is('null') // guard for 'null'All basic type strings:
| String | 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) |
Basic guards will return false for objects created with constructors. For example,
is('string')(new String())returnsfalse. UseisInstanceOfinstead.
Union types
Every guard has an or method with the same signature as is. You can use or to create union types:
const isStringOrNumber = is('string').or('number') // guard for 'string | number'
isStringOrNumber('') // true
isStringOrNumber(0) // true
isStringOrNumber(true) // falseLiteral 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 isCat = isLiterally('cat') // guard for '"cat"'
const is5 = isLiterally(5) // guard for '5'
const isTrue = isLiterally(true) // guard for 'true'
const isCatOr5 = isLiterally('cat').orLiterally(5) // guard for '"cat" | 5'
const isCatOr5OrTrue = isLiterally('cat', 5, true) // guard for '"cat" | 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
isStrOrNumArrwhich is a guard for(string | number)[], andisStrArrOrNumArrwhich is a guard forstring[] | number[].
For basic array types, you can simply pass a string to the is function instead of using isArrayOf:
import { is } from 'ts-guardian'
const isStrArr = is('string[]') // guard for 'string[]'
const isStrArrOrNumArr = is('string[]').or('number[]') // 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 a tuple to is:
const isStrNumTuple = is(['string', 'number']) // guard for '[string, number]'
isStrNumTuple(['high']) // false
isStrNumTuple(['high', 5]) // trueGuards for nested tuples can be defined by nesting tuple guards inside tuple guards:
const isStrAndNumNumTupleTuple = is(['string', is(['number', 'number'])]) // guard for '['string', [number, number]]'Object types
Use is({}) to check for any non-null object. Avoid is('object') as it matches null:
const isObject = is({}) // guard for '{}'
isObject({ some: 'prop' }) // true
isObject(null) // falseTo 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: 'John' }) // false
hasAge({ name: 'John', age: 40 }) // trueIntersection types
Every type guard has an and method which has the same signature as or. Use and to create intersection types:
const hasXOrY = is({ x: 'any' }).or({ y: 'any' }) // guard for '{ x: any; } | { y: any; }'
hasXOrY({ x: '' }) // true
hasXOrY({ y: '' }) // true
hasXOrY({ x: '', y: '' }) // true
const hasXAndY = is({ x: 'any' }).and({ y: 'any' }) // guard for '{ x: any; } & { y: any; }'
hasXAndY({ x: '' }) // false
hasXAndY({ y: '' }) // false
hasXAndY({ x: '', y: '' }) // trueInstance 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()) // true
const isRegExpOrUndefined = is('undefined').orInstanceOf(RegExp) // guard for 'undefined | RegExp'
isRegExpOrUndefined(/./) // true
isRegExpOrUndefined(new RegExp('.')) // true
isRegExpOrUndefined(undefined) // trueThis works with user-defined classes too:
class Person {
name: string
constructor(name: string) {
this.name = name
}
}
const john = new Person('John')
const isPerson = isInstanceOf(Person) // guard for 'Person'
isPerson(john) // trueOptional and nullable types
Use isOptional, isNullable, and isNullish to quickly create guards for optional and nullable types:
import { isOptional, isNullable, isNullish } from 'ts-guardian'
const isOptionalNumber = isOptional('number') // guard for 'number | undefined'
const isNullableNumber = isNullable('number') // guard for 'number | null'
const isNullishNumber = isNullish('number') // guard for 'number | null | undefined'For optional basic types, you can simply pass a string to the is function instead of using isOptional:
import { is } from 'ts-guardian'
const isOptionalString = is('string?') // guard for 'string | undefined'
const isOptionalNumberArray = is('number[]?') // guard for 'number[] | undefined'Parsing to user-defined types
Consider the following type and its guard:
type Book = {
title: string
author: string
}
const isBook = is({
title: 'string',
author: 'string',
})If isBook returns true for a value, that value will be typed as:
{
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 it is tedious.
Instead, you can use 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) // book as type 'Book'
parseBook(film) // undefinedThe parserFor function is 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 isStringOrNumber = isString.or('number') // guard for 'string | number'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)Throwing
Use the requireThat function to throw an error if a value does not match a guard:
import { is, requireThat } 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.
requireThat(value, is('string'))
// Otherwise, type of value is 'string'
value.toUpperCase()You can optionally pass an error message to requireThat:
import { isUser } from '../myTypeGuards/isUser'
requireThat(value, isUser, 'Value is not a user!')Type-safe type guards
Consider the following problem:
You fetch data from an API. How do you ensure it's a valid User before using it?
The User type:
type User = {
id: number
name: string
email?: string
phone?: {
primary?: string
secondary?: string
}
teamIds: string[]
}Solution 1 - User-defined type guards 👎
With TypeScript's user-defined type guards, you could write an isUser function to confirm the value is of type User. It might look something like this:
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) &&
Array.isArray(u.teamIds) &&
u.teamIds.every(teamId => typeof teamId === 'string')
)
}Hard to read, but it works. However, you could also write:
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 because type predicates are effectively type assertions.
Using type predicates means you potentially lose type safety and introduce runtime errors into your app.
Solution 2 - Primitive-based type guards 👍
Rather than making assumptions about the value, you define a primitive-based type of what a User object looks like, and rely on TypeScript to determine compatibility with the User type:
A primitive-based type is a type constructed from only primitive TypeScript types (
string,number,undefined,any, etc...).
import { is, isOptional } from 'ts-guardian'
// We make no assumptions that the data is a user-defined type
const isUser = is({
id: 'number',
name: 'string',
email: 'string?',
phone: isOptional({
primary: 'string?',
secondary: 'string?',
}),
teamIds: 'string[]',
})This is much more readable, and more importantly, it's 100% type-safe.
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
teamIds: string[]
}Now TypeScript will tell you if this type is compatible with User:
// TypeScript complains if the primitive-based type is not compatible with 'User'
const parseUser = parserFor<User>(isUser)If the type from isUser is not compatible with the User, a TypeScript compiler error will let you know. 🎉
