Git Product home page Git Product logo

type-guards's Introduction

type-guards

This module allows you to write run-time validation in a clear way in a strongly-typed manner. In other words, it gives you a nice way to keep your code DRY by writing the validation function, and get the TypeScript types with it.

Installation

$ yarn add type-guards

Examples

import * as tg from 'type-guards'
const isUser = tg.isOfShape({
  name: tg.isString,
  age: tg.isNumber,
})

// we purposely mark it as "any" to imitate API response or user's input
// (anything generated at runtime which we cannot give a static type to)
const john: any = { name: 'John', age: 21 }

if (isUser(john)) {
  john.name // typesafe, this is a string
  john.age // typesafe, this is a number
  john.years // error
}

List of functions

is

Create a validator that asserts that the given argument is strictly equal (===) to something. Not very useful on its own.

isOneOf

Create a validator that asserts that at least one of the validators passed as arguments are passing.

const isAbc = isOneOf(is('a'), is('b'), is('c'))
isAbc('a') // => true
isAbc('b') // => true
isAbc('c') // => true
isAbc('d') // => false

isEnum

Create a validator that asserts that the given arguments is exactly one of the given arguments. For example, the validator form the previous example can be written in a more simple way.

const isAbc = isEnum('a', 'b', 'c')

If all the arguments are of the same type, it will be inferred; so the above example will assert for string. If you have an enum, list all its values somehow in an array, and use the spread operator to pass the values in.

enum Gender { Male = 'm', Female = 'f' }
const GENDERS = [ Gender.Male, Gender.Female ]
const isGender = isEnum(...GENDERS) // a guard for type Gender

isNull

A validator that asserts that the given argument is null. Short for is(null)

isUndefined

A validator that asserts that the given argument is undefined. Short for is(undefined).

isNullOrUndefined, isNullish

A validator that asserts that the given argument is null or undefined (like doing arg == null). Short for isOneOf(is(null), is(undefined)).

An alias with a shorten yet recognizable name is isNullish.

isNotNull, isNotUndefined, isNotNullOrUndefined, isNotNullish

The opposite of the previous three validators.

A common use-case is filtering an array to get rid of nullish values:

const array: Array<number | null | undefined> = [0, null, 1, undefined, 2]
const filtered = array.filter(tg.isNotNullish)
// type of `filtered` is `Array<number>`

Doesn't work perfectly with the else branch, but this is a less common use-case. Either way, help is appreciated in the SO thread if you know more about this.

isOfBasicType

Create a validator that asserts that the given argument is of a certain type. This is a wrapper around typeof checks and works with string, number, boolean, symbol, function and object.

isString, isNumber, isBoolean, isSymbol, isObject, isFunction.

Validators that assert that the given argument is of the correct type. Short for isOfBasicType('string'), isOfBasicType('number'), etc.

Instead of isObject, you probably need isShapeOf instead, which gives you more control over the type.

isInstanceOf

Create a validator that asserts that utilized the mechanism of instanceof keyword in JavaScript.

isArrayOf

Create a validator that asserts the given argument is an array, where each of its item is of a certain type. The type of the items is passed as the argument of isArrayOf.

const areNumbers = isArrayOf(isNumber)
areNumbers([1, 2, 3]) // => true
areNumbers(1) // => false
areNumbers([1, 2, '3']) // => false
areNumbers([1, 2, undefined]) // => false

Of course, feel free to combine validators.

const areKindaNumbers = isArrayOf(isOneOf(isNumber, isNullOrUndefined))
areNumbers([1, 2, 3]) // => true
areNumbers([1, 2, null, 4, undefined]) // => true

isOfShape

Create a validator that asserts that the given argument is an object, where each of the values of its keys correspond to the given shape. The shape is an object where the values are either new shapes or simple type checks. isOfShape allows objects that have extra keys. See isOfExactShape to exclude objects having extra keys not defined by the shape.

const isUser = isOfShape({ name: isString, age: isNumber })
isUser({name: 'John', age: 21}) // => true
isUser({name: 'John', years: 21}) // => false
isUser({name: 'John', age: 21, years: 21}) // => true
isUser({name: 'John'}) // => false

const isCompany = isOfShape({
  name: isString,
  users: isArrayOf(isUser),
})
isCompany({name: 'Untitled', users: [{name: 'John', age: 21}]) // => true

isOfExactShape

The same as isOfShape, except that it excludes objects that have extra keys not defined by the shape.

const isUser = isOfExactShape({ name: isString, age: isNumber })
isUser({name: 'John', age: 21}) // => true
isUser({name: 'John', years: 21}) // => false
isUser({name: 'John', age: 21, years: 21}) // => false
isUser({name: 'John'}) // => false

isTuple

Create a validator that asserts that passed argument is a tuple of certain elements.

const isNamePair = isTuple(isString, isString)

isNamePair(['Walter', 'Jessie']) // => true
isNamePair('Gustavo') // => false
isNamePair(['Hector']) // => false
isNamePair(['Walter', 'Jessie', 'Mike']) // => false

pick

Create a validator which utilizes an already created validator and picks only a part of it.

const fullUser = isOfShape({ name: isString, age: isNumber })
const partOfUser1 = pick(fullUser, 'name')
const partOfUser2 = isOfShape({ name: isString })
// the two consts above produce the same validator

omit

Create a validator which utilizes an already created validator and omits a part of it.

const fullUser = isOfShape({ name: isString, age: isNumber })
const partOfUser1 = omit(fullUser, 'age')
const partOfUser2 = isOfShape({ name: isString })
// the two consts above produce the same validator

partial

Create a validator which utilizes an already created validator but allows undefined for every value.

const fullUser = isOfShape({ name: isString, age: isNumber })
const partOfUser1 = partial(fullUser)
const partOfUser2 = isOfShape({ name: one(isUndefined, isString), age: isOneOf(isUndefined, isNumber) })
// the two consts above produce the same validator

Note, however:

const partOfUser = partial(fullUser)
partOfUser({ name: 'John', age: 21 }) // => true
partOfUser({ name: 'John' }) // => false (missing "age")
partOfUser({ name: 'John', age: undefined }) => true 

Currently working on making the second one return true as well.

Using the type only

If you do not need to use the validator, but only want the type information, you can do that as well.

type Number = FromGuard<typeof isNumber> // : number

const isUser = isOfShape({ name: isString, age: isNumber })
type User = FromGuard<typeof isUser> // : { name: string, age: number }

This means that your codebase can have validators "just in case", but if you never use them, it will not increase your bundle size. You could also set up your build pipeline in such way that the validators are run only in development mode.

Run-time assertion

You usually want to throw an exception at run-time in case the state of the application becomes unexpected. For example, you might have public foo?: string in the class, but at some place you're certain that foo must be defined. Instead of doing this.foo!, which is just a build-time assertion, you might want to perform a run-time assertion such as the following.

if (this.foo === undefined) {
  throw new Error(`Unexpected value "undefined".`) 
}

TypeScript will properly assert here that this.foo is Exclude<string | undefined, undefined> below the if block, which boils down to string.

However, this becomes quite annoying to write all the time. Hence, throwIf helper.

const foo = tg.throwIf(tg.isUndefined)(this.foo)

Or, create a reusable function. This is the recommended way.

const throwIfUndefined = tg.throwIf(tg.isUndefined, `Unexpected "undefined" value.`)
const foo = throwIfUndefined(this.foo, `"this.foo" should've been defined here. Something's wrong.`)

type-guards's People

Contributors

andrijapesic avatar barberducky avatar jkostov avatar justinsc avatar kkirsche avatar lazarljubenovic avatar michsior14 avatar pritilender avatar rebeccastevens avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

type-guards's Issues

Add "isTuple" typeguard

Something like this:

function isTuple<A>(a: Guard<A>): Guard<[A]>
function isTuple<A, B>(a: Guard<A>, b: Guard<B>): Guard<[A, B]>
function isTuple<A, B, C>(a: Guard<A>, b: Guard<B>, c: Guard<C>): Guard<[A, B, C]>

For convenience, maybe add this too:

function isTuple<A>(n: 1, a: Guard<A>): Guard<[A]>
function isTuple<A>(n: 2, a: Guard<A>): Guard<[A, A]>
function isTuple<A>(n: 3, a: Guard<A>): Guard<[A, A, A]>

Utilize the new `asserts` feature of TS 3.7

From here:

function assertIsString(val: any): asserts val is string {
    if (typeof val !== "string") {
        throw new AssertionError("Not a string!");
    }
}

Brainstorm todo list

  • Check if it's convenient to use the library with a simple consumer-created assert method, such as assert(tg.isString), or should type-guards provide the assert function with an easy-to-use signature with type guards provided by the library.
  • Is it needed to provide things like tg.assertIsString, or should the above assert(tg.isString) or tg.assert(tg.isString) enough?

Add "isAny" guard

It would always return true. Mostly useful for composition, eg. tg.isArrayOf(tg.isAny).

Add "throwUnless"

Instead of writing tg.throwIf(tg.isNot(tg.isString)), it would be nicer to write tg.throwUnless(tg.isString).

Is this project still alive?

type-guards is an indispensable library for writing type-safe codes.
However, the major version of it is still 0 and has not been updated for a long time.
Is this project still being maintained?

Thank you for creating awesome library.

Export extended Guard type

The lib currently exports Guard with only one generic argument: (x: any) => x is T, but I found that in code I often need (x: V) => x is T. Instead of exporting Guard<T>, we should export Guard<T, V = any>. This is not a breaking change since the second one is optional.

Add a general signature for `or` (no guards)

Today I had an array of functions which were not guards, and I couldn't use tg.fp.or on them, because the signature requires a guard.

export function numberEndsIn (desiredLastDigits: string) {
  return (number: number | string): boolean => {
    const numberString = number.toString()
    return numberString.endsWith(desiredLastDigits)
  }
}

export function numberEndsInOneOf (...desiredLastDigits: string[]) {
  return (tg.fp.or as any)(...desiredLastDigits.map(numberEndsIn))
}

There's no reason not allow usage of these fp helpers, even when no guards are used.

"isShapeOf" should have an option to allow additional properties

For example, the API could return some additional information; the guard shouldn't fail because the object contains more information. It's important that nothing's missing or is of a wrong type.

This should probably be the default behavior, with a "strict" option to return to the current one.

isEnum should also accept an enumeration object

Instead of writing

export const isLanguageCode = tg.isEnum(...enumValues(LanguageCode))

where enumValues is a function defied as follows

export function enumValues<T extends object> (enumeration: T): Array<T[keyof T]> {
  return objectKeys(enumeration)
    .filter(k => isNaN(Number(k)))
    .map(k => enumeration[k])
}

allow the consumer to simply write

export const isLanguageCode = tg.isEnum(LanguageCode)

Maybe deprecate listing all values and create a new guard for that?

Add "throwIf" helper

export function throwIfUndefined<T> (t: T | undefined,
                                     err: string = `Expected value not to be undefined`): Exclude<T, undefined> {
  if (t === undefined) throw new Error(err)
  return t as Exclude<T, undefined>
}

export function throwIfNullish<T> (t: T | undefined | null,
                                   err: string = `Expected value not to be undefined or null`): T {
  if (t == null) {
    throw new Error(err)
  }
  return t
}

A helper to easily create these by passing in a type guard, something like const throwIfUndefined = tg.throwIf(tg.isUndefined), which would also infer the type properly and strip away the "throw" ones.

Rename "oneOf" to "isOneOf"

What kind of devil has possessed me when I named it oneOf instead of isOneOf, I don't know. Literally every other has is prefix.

Don't forget to update the documentation as well.

Filtering out specific symbols

Symbols are great for special values where "null" and/or "undefined" already represent a valid value. Since they are unique, there is no fear of "what if the consumer wants exactly that value?"-type of questions. Of course, there are other possibilities such as wrapping the unknown-yet-highly-customizable value in an object such as { isReady: false } | { isReady: true, value: T}; however, another equally valid approach is T | typeof NOT_READY, where const NOT_READY = Symbol('NOT_READY').

When we want to filter out these "special" values, for one reason or another (ignoring them, skipping them, waiting only for them), we might encounter code like this:

filter(event => event != NOT_READY)

Obviously, this is not strict enough and would require a specific guard. This is precisely what this module was created for, and while tg.isNot(NOT_READY) seems like it should work, it doesn't. Not sure if bug, or ignorance of Symbols. Either way, would like to have this.

"isOfShape" should allow keys not to be specified if that key can be undefined

import {
  isNumber,
  isOfShape,
  isOneOf,
  isUndefined
} from 'type-guards';

const isFoo = isOfShape({
  bar: isOneOf(isNumber, isUndefined),
  baz: isNumber
});

// This is ok.
console.log(isFoo({ bar: undefined, baz: 123 }));

// But this doesn't work.
console.log(isFoo({ baz: 123 }));
// Expect result: true
// Actual result: false

Add "not" helper to the FP module

I think it's not gonna be straightforward at all to cover all cases with overloads. But it should at least work nicely with most of the guards.

I'm not even sure how to manually write the type for "everything but not a number". Ideally, the else branch also needs to work, where it would be a number.

Increase arguments for "or" and "and"

This can go on forever, but 10 is a nice upper boundary, I think.

Currently working around with nesting ors, such as:

export const isRootLevel = tg.fp.or(
  isText,
  isInterpolation,
  tg.fp.or(
    isElement,
    isNgTemplate,
    isNgContainer,
  ),
)

Allow missing properties when `undefined` would be a valid value?

For a majority of usecases, the following types are treated as equal:

interface Type1 {
  x?: number
}

interface Type2 {
  x: number | undefined
}

The difference that Type2 requires x to be present on the object, while Type1 allows the object to be empty. There's no difference when accessing type.x -- the inherited type of such expression would be number | undefined in both cases.

However, the observable difference occurs when iterating over keys. Object.keys(type) would in first case return either [] or ['x'], while in the second case it's guaranteed that the result would be ['x'], exclusively.

type-guards currently cannot handle the optional argument right now, forcing the developer to write things like this.

const isType = tg.isOfShape({
  x: tg.fp.or(tg.isUndefined, tg.isNumber),
})

type Type = tg.FromGuard<typeof isType>

The issue here is that Type would be same as Type2, while the current behavior of the library is that the run-time guard is the same as Type1 (x is required to exist on the object, even if its value is undefined).

Maybe it would be a good idea for both the runtime guard and the static type to result in the type Type1 | Type2. In cases when a key is required to exist (but its value is allowed to be undefined), the developer would need to make additional runtime checks, without option to rely on static types. I'm not sure yet if this is a good compromise.

Types which differ according to "type" or "kind"

const isMorphRuleOfType = <TMorphType extends MorphType> (type: TMorphType) => {
  return (rule: MorphRuleBase<MorphType>): rule is MorphRuleBase<TMorphType> => {
    return rule.type === type
  }
}

Think of a way to quickly create this. Something like hasType('type', type).

Add a utility for merging two types (union, &)

With types:

type A = { a: number }
type B = { b: number }
type C = A & B

With type guards:

const isA = tg.isOfShape({ a: tg.isNumber })
const isB = tg.isOfShape({ a: tg.isBoolean })
const isC = ?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.