UNPKG

ts-guardian

Version:
530 lines (378 loc) 15 kB
[![MIT license][license-badge]][license] [![NPM version][npm-badge]][npm] # ts-guardian **Runtime type guards. Composable, TypeScript-like syntax. 100% type-safe.** Full TypeScript support _(TypeScript not required)_. <br /> ### 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. <br /> ## Installation ``` npm install ts-guardian ``` <br /> ## Quick Start Import the `is` function and create a type-guard, such as `isUser`. Use that type-guard to confirm objects match the type: ```ts import { is } from 'ts-guardian' const isUser = is({ id: 'number', name: 'string', email: 'string?', teamIds: 'number[]', }) isUser({ id: 1, name: 'John', teamIds: [2, 3] }) // true ``` <br /> ### Core 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](#type-safe-type-guards). <br /> ## API - [`is` function](#is-function) - [Basic types](#basic-types) - [Union types](#union-types) - [Intersection types](#intersection-types) - [Literal types](#literal-types) - [Array types](#array-types) - [Object types](#object-types) - [Tuple types](#tuple-types) - [Instance types](#instance-types) - [Optional and nullable types](#optional-and-nullable-types) - [Parsing to user-defined types](#parsing-to-user-defined-types) - [Composition](#composition) - [Throwing](#throwing) <br /> ### `is` function ```ts 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: ```ts const isNumber = is('number') // guard for 'number' isNumber(0) // true isNumber('') // false ``` <br /> ### Basic types Pass a type string to create guards for basic types: ```ts 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())` returns `false`. Use [`isInstanceOf`](#instance-types) instead. <br /> ### Union types Every guard has an `or` method with the same signature as `is`. You can use `or` to create union types: ```ts const isStringOrNumber = is('string').or('number') // guard for 'string | number' isStringOrNumber('') // true isStringOrNumber(0) // true isStringOrNumber(true) // false ``` <br /> ### 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: ```ts 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' ``` <br /> ### Array types To check that every element in an array is of a specific type, use the `isArrayOf` function and the `orArrayOf` method: ```ts 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 `isStrOrNumArr` which is a guard for `(string | number)[]`, and `isStrArrOrNumArr` which is a guard for `string[] | number[]`. For basic array types, you can simply pass a string to the `is` function instead of using `isArrayOf`: ```ts import { is } from 'ts-guardian' const isStrArr = is('string[]') // guard for 'string[]' const isStrArrOrNumArr = is('string[]').or('number[]') // guard for 'string[] | number[]' ``` <br /> ### Record types To check that every value in an object is of a specific type, use the `isRecordOf` function and the `orRecordOf` method: ```ts 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>' ``` <br /> ### Tuple types Guards for tuples are defined by passing a tuple to `is`: ```ts const isStrNumTuple = is(['string', 'number']) // guard for '[string, number]' isStrNumTuple(['high']) // false isStrNumTuple(['high', 5]) // true ``` Guards for nested tuples can be defined by nesting tuple guards inside tuple guards: ```ts const isStrAndNumNumTupleTuple = is(['string', is(['number', 'number'])]) // guard for '['string', [number, number]]' ``` <br /> ### Object types Use `is({})` to check for any non-null object. Avoid `is('object')` as it matches null: ```ts const isObject = is({}) // guard for '{}' isObject({ some: 'prop' }) // true isObject(null) // false ``` To create a guard for an object with specific members, define a guard for each member key: ```ts const hasAge = is({ age: 'number' }) // guard for '{ age: number; }' hasAge({ name: 'John' }) // false hasAge({ name: 'John', age: 40 }) // true ``` <br /> ### Intersection types Every type guard has an `and` method which has the same signature as `or`. Use `and` to create intersection types: ```ts 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: '' }) // true ``` <br /> ### Instance types Guards for object instances are defined by passing a constructor object to the `isInstanceOf` function and the `orInstanceOf` method: ```ts 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) // true ``` This works with user-defined classes too: ```ts class Person { name: string constructor(name: string) { this.name = name } } const john = new Person('John') const isPerson = isInstanceOf(Person) // guard for 'Person' isPerson(john) // true ``` <br /> ### Optional and nullable types Use `isOptional`, `isNullable`, and `isNullish` to quickly create guards for optional and nullable types: ```ts 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`: ```ts import { is } from 'ts-guardian' const isOptionalString = is('string?') // guard for 'string | undefined' const isOptionalNumberArray = is('number[]?') // guard for 'number[] | undefined' ``` <br /> ### Parsing to user-defined types Consider the following type and its guard: ```ts 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: ```ts { title: string author: string } ``` Ideally, we want to type the value as `Book`, while [avoiding type assertions and user-defined type predicates](#type-safe-type-guards). One way is with a parse function that utilizes TypeScript's implicit casting: ```ts 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: ```ts 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`: ```ts const book = { title: 'Odyssey', author: 'Homer', } const film = { title: 'Psycho', director: 'Alfred Hitchcock', } parseBook(book) // book as type 'Book' parseBook(film) // undefined ``` The `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: ```ts const parseBook = parserFor<Book>(isBook) // Fine const parseBook = parserFor<Book>(isString) // TypeScript error - type 'string' is not assignable to type 'Book' ``` <br /> ### Composition Guards can be composed from existing guards: ```ts const isString = is('string') // guard for 'string' const isStringOrNumber = isString.or('number') // guard for 'string | number' ``` You can even pass guards into `or`: ```ts 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) ``` <br /> ### Throwing Use the `requireThat` function to throw an error if a value does not match a guard: ```ts 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`: ```ts import { isUser } from '../myTypeGuards/isUser' requireThat(value, isUser, 'Value is not a user!') ``` <br /> ## 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: ```ts 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][user-defined-type-guards], you could write an `isUser` function to confirm the value is of type `User`. It might look something like this: ```ts 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: ```ts 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...). ```ts 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: ```ts // 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`: ```ts // 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. 🎉 <br /> [license-badge]: https://img.shields.io/badge/license-MIT-informational.svg [license]: license.md [npm]: https://npmjs.org/package/ts-guardian [npm-badge]: https://badge.fury.io/js/ts-guardian.svg [user-defined-type-guards]: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards [typescript]: https://www.typescriptlang.org/docs [functional-programming]: https://en.wikipedia.org/wiki/Functional_programming