idonttrustlikethat
    TypeScript icon, indicating that this package has built-in type declarations

    2.1.0 • Public • Published

    idonttrustlikethat

    This module helps validating incoming JSON, Form values, url params, localStorage values, server Environment objects, etc in a concise and type safe manner.
    The focus of the lib is on small size and an easy API to add new validations.

    Note: This module uses very precise Typescript types. Thus, it is mandatory to at least have the following tsconfig / tsc's compiler options flag: strict: true.

    How to

    Create a new validation

    This library exposes a validator for all primitive and object types so you should usually start from one of these then compose it with extra validations.

    Here's how isoDate is defined internally:

    import { string, Err, Ok } from 'idonttrustlikethat'
    
    const isoDate = string.and(str => {
      const date = new Date(str)
      return isNaN(date.getTime())
        ? Err(`Expected ISO date, got: ${pretty(str)}`)
        : Ok(date)
    })
    
    isoDate.validate('2011-10-05T14:48:00.000Z').ok // true

    This creates a new Validator that reads a string then tries to create a Date out of it.

    You can also create an optional validation step that wouldn't make sense on its own:

    import { string, Err, Ok, array, string } from 'idonttrustlikethat'
    
    // This is essentially a basic filter() but with a nicer, custom error message.
    const minSize = (size: number) => <T>(array: T[]) =>
      array.length >= size
        ? Ok(array)
        : Err(`Expected an array with at least ${size} items`)
    
    const bigArray = array(string).and(minSize(100))
    bigArray.validate(['1', '2']).ok // false

    Note: the minSize validator does exactly that, but for more input types.

    If you need to start from any value, you can use the unknown validator that always succeeds.

    Deriving the typescript type from the validator type

    This can be used with any combination of validators except ones using recursion.

    You can get the exact type of a validator's value easily:

    import { object, string, number } from 'idonttrustlikethat'
    
    const person = object({
      name: string,
      age: number,
    })
    
    type Person = typeof person.T
    
    const person: Person = {
      name: 'Jon',
      age: 80
    }

    Customize error messages

    If you say, use this library to validate a Form data, it's best to assign your error messages directly in the validator so that the proper error messages get accumulated, ready for you to display them.

    import { object, string } from 'idonttrustlikethat'
    
    const mandatoryFieldError = 'This field is mandatory'
    const mandatoryString = string.withError(_ => mandatoryFieldError)
    
    const formValidator = object({
      name: mandatoryString,
    })
    
    // {ok: false, errors: [{path: 'name', message: 'This field is mandatory'}]}
    const result = formValidator.validate({})

    Perform async checks

    You don't! Some "similar" libraries offer this functionality but it's a pretty bad idea. It accumulates concerns inside your validation layer (you now have to pass DB connections, API tokens, etc to what should be dumb validators) and polutes the API signatures (once you go async for a tiny bit, everything now has to be async)

    For instance, instead of trying to make a call to the DB to check some unicity constraint inside your validator, instead prepare the call's result before hand then pass that to a function that creates a new validator using that result, for instance:

    import {string, object} from 'idonttrustlikethat' 
    
    function makeUserValidator(params: {isEmailKnown: boolean}) {
      const {isEmailKnown} = params
    
      return object({
        name: string,
        email: string
          .withError(_ => 'The email is mandatory')
          .filter(_ => !isEmailKnown)
          .withError(_ => 'This email is already in use')
      })
    }
    
    const isEmailKnown = await db.user.checkIfEmailIsKnown(...)
    
    const validatedUser = makeUserValidator({isEmailKnown}).validate(body)

    Exports

    Here are all the values this library exposes:

    import {
      Err,
      Ok,
      array,
      dictionary,
      errorDebugString,
      intersection,
      union,
      is,
      literal,
      unknown,
      null as vnull,
      number,
      object,
      string,
      boolean,
      tuple,
      undefined,
    } from 'idonttrustlikethat'
    import {
      isoDate,
      recursion,
      snakeCaseTransformation,
      relativeUrl,
      absoluteUrl,
      url,
      booleanFromString,
      numberFromString,
      intFromString,
      minSize,
      nonEmpty
    } from 'idonttrustlikethat'

    And all the types:

    import {
      Result,
      Err,
      Ok,
      Validation,
      Validator,
      Configuration,
    } from 'idonttrustlikethat'

    API

    validate

    Every validator has a validate function which returns a Result (either a {ok: true, value} or a {ok: false, errors}) Errors are accumulated.

    import { object, errorDebugString } from 'idonttrustlikethat'
    
    const myValidator = object({})
    const result = myValidator.validate(myJson)
    
    if (result.ok) {
      console.log(result.value)
    } else {
      console.error(errorDebugString(result.errors))
    }

    In case of errors, errors contains an Array of { message: string, path: string } where message is a debug error message for developers and path is the path where the error occured (e.g people.0.name)

    errorDebugString will give you a complete debug string of all errors, e.g.

    At [root / c] Error validating the key. "c" is not a key of {
      "a": true,
      "b": true
    }
    At [root / c] Error validating the value. Type error: expected number but got string
    

    primitives

    import * as v from 'idonttrustlikethat'
    
    v.unknown
    v.string
    v.number
    v.boolean
    v.null
    v.undefined
    
    v.string.validate(12).ok // false

    tagged string/number

    Sometimes, a string or a number is not just any string or number but carries extra meaning, e.g: email, uuid, UserId, KiloGram, etc.
    Tagging such a primitive as soon as it's being validated can help make the downstream code more robust and better documented.

    import { string, object } from 'idonttrustlikethat'
    
    type UserId = string & { __tag: 'UserId' } // Note: You can use any naming convention for the tag.
    
    const userId = string.tagged<UserId>()
    
    const user = object({
      id: userId
    })

    If you don't use tagged types, it can lead to situations like:

    const user = object({
      id: string,
      companyId: string
    })
    
    const user = {
      id: '12345678',
      companyId: '7cd3821a-553f-4d26-84f9-88776005612b'
    }
    
    function fetchCompanyDetails(companyId: string) {}
    
    // Nothing prevents you from passing the wrong ID "type"
    fetchCompanyDetails(user.id)

    Using tagged types fixes all these problems while also retaining that type's usefulness as a basic string/number.

    literal

    import { literal } from 'idonttrustlikethat'
    
    // The only value that can ever pass this validation is the 'X' string literal
    const validator = literal('X')

    object

    import { string, object, union } from 'idonttrustlikethat'
    
    const person = object({
      id: string,
      prefs: object({
        csvSeparator: union(',', ';', '|').optional(),
      }),
    })
    
    validator.validate({
      id: '123',
      prefs: {},
    }).ok // true

    Note that if you validate an input object with extra properties compared to what the validator know, these will be dropped from the output.
    This helps keeping a clean object and let us avoid dangerous situations such as:

    import { string, object } from 'idonttrustlikethat'
    
    const configValidator = object({
      clusterId: string,
      version: string
    })
    
    const config = {
      clusterId: '123',
      version: 'v191',
      extraStuffFromTheServer: 100,
      _metadata: true
    }
    
    // Let's imagine what could happen if this kept all non declared properties in the output.
    const result = configValidator.validate(config)
    
    if (result.ok) {
      // As far as typescript is concerned, all values are string in the validated object, which let us manipulate it as such, perhaps to pass it some generic utility:  
      const configDictionary: Record<string, string> = result.value
    
      // But it's a lie, some properties are still found in the object that aren't strings.
      // This will throw an exception when the entire point of validating is to avoid that.
      Object.values(configDictionary).forEach(str => str.padStart(2))
    }

    array

    import { array, string } from 'idonttrustlikethat'
    
    const validator = array(string)
    
    validator.validate(['a', 'b']).ok // true

    tuple

    import { tuple, string, number } from 'idonttrustlikethat'
    
    const validator = tuple(string, number)
    
    validator.validate(['a', 1]).ok // true

    union

    import { union, string, number } from 'idonttrustlikethat'
    
    const stringOrNumber = union(string, number)
    
    validator.validate(10).ok // true

    Unions of literal values do not have to use literal() but can be passed the values directly:

    import {union} from 'idonttrustlikethat'
    
    const bag = union(null, 'hello', true, 33)

    discriminatedUnion

    Although you could also use union for your discriminated unions, discriminatedUnion is faster and has better error messages for that special case. It will also catch common typos at the type level.
    Note that discriminatedUnion only works with object and intersection (of objects) validators. Also, the discriminating property must be either a literal or union of primitives.

    import {discriminatedUnion, literal, string} from 'idonttrustlikethat'
    
    const userSending = object({
      type: literal('sending')
    })
    
    const userEditing = object({
      type: literal('editing'),
      currentText: string
    })
    
    const userChatAction = discriminatedUnion('type', userSending, userEditing)

    intersection

    import { intersection, object, string, number } from 'idonttrustlikethat'
    
    const object1 = object({ id: string })
    const object2 = object({ age: number })
    const validator = intersection(object1, object2)
    
    validator.validate({ id: '123', age: 80 }).ok // true

    optional, nullable

    optional() transforms a validator to allow undefined values.

    nullable() transforms a validator to allow undefined and null values, akin to the std lib NonNullable type.

    If you must validate a T | null that shouldn't possibly be undefined, you can use union()

    import { string } from 'idonttrustlikethat'
    
    const validator = string.nullable()
    
    const result = validator.validate(undefined)
    
    result.ok && result.value // undefined

    default

    Returns a default value if the validated value was either null or undefined.

    import { string } from 'idonttrustlikethat'
    
    const validator = string.default(':(')
    
    const result = validator.validate(undefined)
    
    result.ok && result.value // :(

    withError

    Sets a custom error message onto the validator.
    The validator have decent error messages by default for developers but you will sometimes want to customize these.
    Note that the first withError encountering an error wins but a single withError will apply to any error encountered in the chain.

    import {object, string} from 'idonttrustlikethat'
    
    const validator = object({
      id: string
        .withError(i => `Expected a string, got ${i}`) // This will activate if the input is not a string or is missing.
        .and(nonEmpty())
        .withError(_ => `The id cannot be the empty string`) // This will activate only if the id is a string but is empty.
    })

    dictionary

    A dictionary is an object where all keys and all values share a common type.

    import { dictionary, string, number } from 'idonttrustlikethat'
    
    const validator = dictionary(string, number)
    
    validator.validate({
      a: 1,
      b: 2,
    }).ok // true

    If you need a partial dictionary, simply type your values as optional:

    import { dictionary, string, number, union } from 'idonttrustlikethat'
    
    const validator = dictionary(union('a', 'b', 'c'), number.optional())
    
    validator.validate({
      b: 1
    }).ok // true

    map, filter

    import { string } from 'idonttrustlikethat'
    
    const validator = string.filter(str => str.length > 3).map(str => `${str}...`)
    
    const result = validator.validate('1234')
    result.ok // true
    result.value // 1234...

    and

    Unlike map which deals with a validated value and returns a new value, and can return either a validated value or an error.

    import { string, Ok, Err } from 'idonttrustlikethat'
    
    const validator = string.and(str =>
      str.length > 3 ? Ok(str) : Err(`No, that just won't do`)
    )

    then

    then allows the chaining of Validators. It can be used instead of and if you already have the Validators ready to be reused.

    // Validate that a string is a valid number (e.g, query string param)
    const stringToInt = v.string.and(str => {
      const result = Number.parseInt(str, 10)
      if (Number.isFinite(result)) return Ok(result)
      return Err('Expected an integer-like string, got: ' + str)
    })
    
    // unix time -> Date
    const timestamp = v.number.and(n => {
      const date = new Date(n)
      if (isNaN(date.getTime())) return Err('Not a valid date')
      return Ok(date)
    })
    
    const timeStampFromQueryString = stringToInt.then(timestamp)
    
    timeStampFromQueryString.validate('1604341882') // {ok: true, value: Date(...)}

    recursion

    import { recursion, string, array, object } from 'idonttrustlikethat'
    
    type Category = { name: string; categories: Category[] }
    
    const category = recursion<Category>(self =>
      object({
        name: string,
        categories: array(self),
      })
    )

    minSize

    Ensures an Array, Object, string, Map or Set has a minimum size. You can also use nonEmpty.

    import {dictionary, string} from 'idonttrustlikethat'
    import {minSize} from 'idonttrustlikethat'
    
    const dictionaryWithAtLeast10Items = dictionary(string, string).and(minSize(10))

    isoDate

    import { isoDate } from 'idonttrustlikethat'
    
    isoDate.validate('2011-10-05T14:48:00.000Z').ok // true

    url

    Validates that a string is a valid URL, and returns that string.

    import { url, absoluteUrl, relativeUrl } from 'idonttrustlikethat'
    
    absoluteUrl.validate('https://ebay.com').ok // true

    booleanFromString

    Validates that a string encodes a boolean and returns the boolean.

    import { booleanFromString } from 'idonttrustlikethat'
    
    booleanFromString.validate('true').ok // true

    numberFromString

    Validates that a string encodes a number (float or integer) and returns the number.

    import { numberFromString } from 'idonttrustlikethat'
    
    numberFromString.validate('123.4').ok // true

    intFromString

    Validates that a string encodes an integer and returns the number.

    import { intFromString } from 'idonttrustlikethat'
    
    intFromString.validate('123').ok // true

    Configuration

    A Configuration object can be passed to modify the default behavior of the validators:

    Configuration.transformObjectKeys

    Transforms every keys of every objects before validating.

    import {snakeCaseTransformation} from 'idonttrustlikethat'
    
    const burger = v.object({
      options: v.object({
        doubleBacon: v.boolean,
      }),
    })
    
    const ok = burger.validate(
      {
        options: {
          double_bacon: true,
        },
      },
      { transformObjectKeys: snakeCaseTransformation }
    )

    Install

    npm i idonttrustlikethat

    DownloadsWeekly Downloads

    1,531

    Version

    2.1.0

    License

    MIT

    Unpacked Size

    72.7 kB

    Total Files

    6

    Last publish

    Collaborators

    • boubiyeah