JavaScript Programming Node.js

JavaScript doesn't mind what you pass to a function. A string turns up where a number was meant, or an argument is missing altogether, and nothing complains until much later, in some other part of the program, when something odd appears on the screen.

The simplest fix I've found is to check every argument at the top of every function. It isn't a type system, just a line per argument, but it catches the mistake on the line that made it. The checks live in a small module called Security. Let's see how it looks.

Checking every argument

const Security = require('Security')

const Player = {
    SetVolume: (player, level) => {
        Security.Object()(player)
        Security.Number(level, 0, 1)

        player.volume = level
    }
}
A function that checks both of its arguments before doing anything

Now if the volume arrives from a form as a string, the call fails straight away, rather than storing "0.5" for something else to trip over later:

Player.SetVolume(player, "0.5")
//  Error: Expected number; got string: "0.5"

Player.SetVolume(player, 5)
//  Error: Expected number <= 1; got 5
The errors from passing a string, and from a number out of range

The message says what was expected and what actually arrived, and the stack trace points at the call that passed it.

Checks return the value

Every check returns the value it was given, so it can sit inside an assignment or an expression:

//  Check and keep in one line
const name = Security.String(input.name)

//  Add two numbers, checking both
const total = Security.Number(a) + Security.Number(b)
Using the value that a check returns

The basic checks

There's a check for each kind of single value: String, Number and Integer with optional limits, Boolean, Callback for a function, HexString for hex digits, and Defined for anything that isn't null or undefined. Each has its inputs and examples in the reference at the end.

Security.Check covers anything the others don't, with a message of your own:

Security.Check(end > start, 'The end must come after the start')
A check on a condition, with its own message

Optional values

An argument that can be left out is checked with Security.Optional. You give it the check for the value, and get back a check that lets undefined through but holds anything else to that check:

const Text = {
    Pad: (text, length, optionalCharacter) => {
        Security.String(text)
        Security.Integer(length, 0)
        Security.Optional(Security.String)(optionalCharacter)

        const character = optionalCharacter === undefined ? ' ' : optionalCharacter
        return text.padStart(length, character)
    }
}
A function with an optional last argument

The common ones have short forms: OptionalString, OptionalNumber, OptionalInteger and OptionalCallback take the value directly, and OptionalArray and OptionalObject take what's inside first, like the checks in the next section. I also name optional arguments optionalSomething, so the name says it might be missing wherever it's used.

Checks that take a type

Some checks need to know what's inside the value: an array of what, a dictionary of what. These are curried functions. You call them once with the check for the contents, and get back a check for the whole thing:

//  An array of strings
Security.Array(Security.String)(names)

//  A grid: an array of arrays of numbers
Security.Array(Security.Array(Security.Number))(grid)

//  An object whose values are all numbers
Security.Dictionary(Security.Number)(scores)
Array and dictionary checks built from the check for their contents

Security.Array(Security.String) is a check in its own right, so it can go anywhere a check can go, including inside another Security.Array. When something fails, the error is about the element that was wrong: checking [[1,2],[3,"4"]] as a grid stops at the "4" with Expected number; got string: "4".

Typed objects

An object can carry the name of its type in an _is field. Give Security.Object a type name and it checks that the name matches:

const player = { _is: 'Game.Player', name: 'Bob', score: 10 }
const score = { _is: 'Game.Score', value: 10 }

Security.Object('Game.Player')(player)   //  Passes
Security.Object('Game.Player')(score)    //  Invalid data type; got Game.Score, expected Game.Player
Checking an object's type by the name in its _is field

A function that builds the object keeps the _is field right, and checks its own arguments on the way in:

const Game = {
    Player: (name, score) => {
        Security.String(name)
        Security.Integer(score, 0)

        return {
            _is: 'Game.Player',
            name,
            score,
        }
    }
}
A constructor function for the Game.Player type

With no type, Security.Object() accepts any object, and a function too, since functions are often used as objects, but not an array or null. It can also take a function instead of a name, which is handed the value to check however it likes.

One of several options

Sometimes a value is one of a few kinds, each with its own data. Security.OneField checks for an object with exactly one field, whose name is one of the options and whose contents pass that option's check:

Security.OneField(shape, {
    circle: 'Shapes.Circle',
    square: 'Shapes.Square',
})
A shape that's either a circle or a square

{ circle: Shapes.Circle(2) } passes. An object with both a circle and a square fails with Already found option "circle" but there is a second option "square" present. Fields starting with an underscore, like _is, don't count. Each option can be a type name or a check function, so this works as an enumerated type that carries data.

Comparing two values

Security.Equal is for tests. It turns both values into text, with the keys of every object sorted so the order they were built in doesn't matter, and if they differ it fails with the two side by side and a cut line just before the first difference:

Security.Equal(saved, expected, 'Saved player')
Comparing a saved player against the one expected
Saved player; Expected values to be equal;

------------------------------------------------------------------------------

------------------------------------------------------------------------------

|{                                                            | {
|level:1,                                                     | level:1,
|name:"Bob",                                                  | name:"Bob",

------------------ 8< ------------------------------------- 8< ------------------------------------- 8< -------------------

|score:10                                                     | score:12
|}                                                            | }
The failure when the two players' scores differ

An ArrayBuffer is written out as its bytes, and a function with fields attached is written out like an object, so the comparison works on everything the checks deal with.

Knowing what was going on

A stack trace tells you which functions were running when a check failed, but often what you really want to know is which piece of data they were working on. Security.Enter records that:

Security.Enter('loading level 3', () => {
    Security.Enter('reading the map', () => {
        Level.ReadMap(data)
    })
})
Describing what's being done while a callback runs

A failure anywhere inside adds the descriptions to its message:

Error: Expected number; got undefined: undefined
  while: loading level 3
  while: reading the map
A failed check with the descriptions of what was going on

Security.Enter returns whatever its callback returns, and takes its description off the list again even if an error is thrown.

Every failure goes through one function, Security.Fail, which throws the error. It starts with a debugger statement, so with the developer tools open the program stops on the failed check with everything still in scope.

Things to know

  • The checks run on every call, so they cost a little time in the tightest loops.
  • Security.Type is a placeholder: it checks that its type name is a string and accepts any value.
  • Security.Check, Security.Equal and Security.Type return nothing. Every other check returns the value, or what the check it was given returns, so any of them can sit inside an expression.
  • NaN fails Security.Number and Security.Integer, even though its type is number, and null fails Security.Object, even though its type is object.
  • The module has no dependencies. It only calls require to look up a type by name when an object's _is field holds a function rather than a string: 'Game.Player' loads the Game module and walks down to Player, and the two functions have to match.
  • The first line raises Error.stackTraceLimit to 200, so a trace goes back far enough to find the code that passed the bad value.

Reference

Every public function in the module. The checks that take a type, like Security.Array, are called twice: once with the type, which gives back a check, and then with the value.

Security.Check

Fails unless a condition is true. It covers anything the other checks don't.

InputTypeDescription
booleanAnyThe condition
optionalMessageString, optionalThe message to fail with, instead of Check failed
ReturnsNothing
Inputs and return value of Security.Check
//  Check that the range is the right way round
Security.Check(end > start, 'The end must come after the start')

//  Check that the list isn't empty
Security.Check(items.length > 0)
Checking a condition

Security.Defined

Fails if the value is null or undefined. Anything else passes, including 0, false and an empty string.

InputTypeDescription
valueAnyThe value to check
optionalMessageString, optionalThe message to fail with, instead of Value is not defined
ReturnsAnyThe value
Inputs and return value of Security.Defined
//  Check whether the variable holds a value
Security.Defined(options.volume)

//  Check that the user was found, and keep it
const user = Security.Defined(users[id], 'No user with that id')
Checking whether a variable holds a value

Security.Any

Accepts any value. It never fails, but it says at the top of the function that the argument can be anything, rather than leaving it unchecked.

InputTypeDescription
valueAnyThe value
ReturnsAnyThe value
Inputs and return value of Security.Any
const Cache = {
    Set: (cache, key, value) => {
        Security.Object()(cache)
        Security.String(key)
        Security.Any(value)

        cache[key] = value
    }
}
A function with an argument that can be anything

Security.String

Fails unless the value is a string, with a message like Expected string; got number: 3.

InputTypeDescription
valueStringThe value to check
ReturnsStringThe value
Inputs and return value of Security.String
//  Check whether the variable is a string
Security.String(name)

//  Check the title and keep it
const title = Security.String(input.title)
Checking whether a variable is a string

Security.Number

Fails unless the value is a number, and within the limits if there are any. NaN fails with Expected number; got NaN. Leave the smallest as undefined to give only the largest.

InputTypeDescription
valueNumberThe value to check
optionalMinValueNumber, optionalThe smallest value allowed
optionalMaxValueNumber, optionalThe largest value allowed
ReturnsNumberThe value
Inputs and return value of Security.Number
//  Check whether the variable is a number
Security.Number(speed)

//  Check whether the variable is a number from 0 to 1
Security.Number(volume, 0, 1)

//  Check whether the variable is a number of 0 or more
Security.Number(distance, 0)

//  Check whether the variable is a number of 100 or less
Security.Number(percent, undefined, 100)
Checking whether a variable is a number

Security.Integer

Fails unless the value is a whole number, and within the limits if there are any.

InputTypeDescription
valueNumberThe value to check
optionalMinValueInteger, optionalThe smallest value allowed
optionalMaxValueInteger, optionalThe largest value allowed
ReturnsNumberThe value
Inputs and return value of Security.Integer
//  Check whether the variable is an integer
Security.Integer(count)

//  Check whether the variable is an integer from 1 to 10
Security.Integer(level, 1, 10)

//  Check whether the variable is an integer of 0 or more
Security.Integer(index, 0)
Checking whether a variable is an integer

Security.Boolean

Fails unless the value is true or false. Values that only behave like them, such as 0, 1 or an empty string, fail, with a message like Expected boolean; got number: 1.

InputTypeDescription
valueBooleanThe value to check
ReturnsBooleanThe value
Inputs and return value of Security.Boolean
//  Check whether the variable is true or false
Security.Boolean(muted)
Checking whether a variable is true or false

Security.Callback

Fails unless the value is a function, with the message Expected function.

InputTypeDescription
valueFunctionThe value to check
ReturnsFunctionThe value
Inputs and return value of Security.Callback
//  Check whether the variable is a function
Security.Callback(onDone)
Checking whether a variable is a function

Security.CallbackOrString

Fails unless the value is a function or a string, with a message like Expected callback or string; got number.

InputTypeDescription
inputFunction or stringThe value to check
ReturnsFunction or stringThe value
Inputs and return value of Security.CallbackOrString
//  Check whether the label is fixed text, or a function that makes it
Security.CallbackOrString(label)
Checking whether a variable is a function or a string

Security.HexString

Fails unless the value is a string of hex digits, 0 to 9 and a to f in either case, and of the length given if there is one. An empty string fails.

InputTypeDescription
valueStringThe value to check
expectedLengthInteger, optionalThe number of digits it must have
ReturnsStringThe value
Inputs and return value of Security.HexString
//  Check whether the variable is a hex string
Security.HexString(data)

//  Check whether the variable is a hex string of 6 digits
Security.HexString(colour, 6)
Checking whether a variable is a string of hex digits
Security.HexString('12xz')
//  Error: Expected hex string (0-9, a-f, A-F); got: "12xz"

Security.HexString('abcd', 6)
//  Error: Expected hex string of length 6; got length 4: "abcd"
The errors from a character that isn't a hex digit, and from the wrong length

Security.Hex32

Fails unless the value is a string of exactly 32 hex digits, which is 128 bits, the size of an MD5 hash. It's Security.HexString(value, 32).

InputTypeDescription
valueStringThe value to check
ReturnsStringThe value
Inputs and return value of Security.Hex32
//  Check whether the variable is an MD5 hash
Security.Hex32(hash)
Checking whether a variable is 32 hex digits

Security.Hex64

Fails unless the value is a string of exactly 64 hex digits, which is 256 bits, the size of a SHA-256 hash or a random token. It's Security.HexString(value, 64).

InputTypeDescription
valueStringThe value to check
ReturnsStringThe value
Inputs and return value of Security.Hex64
//  Check whether the variable is a SHA-256 hash
Security.Hex64(hash)

//  Check the token that came with a request, and keep it
const token = Security.Hex64(request.token)
Checking whether a variable is 64 hex digits

Security.ArrayBuffer

Fails unless the value is an ArrayBuffer, with the message Check failed. A typed array such as a Uint8Array isn't one, but the buffer it views is.

InputTypeDescription
aArrayBufferThe value to check
ReturnsArrayBufferThe value
Inputs and return value of Security.ArrayBuffer
//  Check whether the variable is an ArrayBuffer
Security.ArrayBuffer(data)

//  Check the buffer behind a Uint8Array
Security.ArrayBuffer(bytes.buffer)
Checking whether a variable is an ArrayBuffer

Security.Buffer

Fails unless the value is an ArrayBuffer or a Node Buffer, with a message like Expected buffer; got string. In a browser, where there's no Buffer, only an ArrayBuffer passes.

InputTypeDescription
bufferArrayBuffer or BufferThe value to check
ReturnsArrayBuffer or BufferThe value
Inputs and return value of Security.Buffer
//  Check whether the variable is a buffer
Security.Buffer(data)

//  Check the buffer behind a Uint8Array
Security.Buffer(bytes.buffer)
Checking whether a variable is a buffer

Security.Array

Makes a check for an array, which fails with a message like Expected array; got string: "abc". Given a check, or a type name, it holds every element to it as well, and fails with the error from the first element that's wrong.

InputTypeDescription
ofCheck or type name, optionalThe check for each element, or the type name each element's _is field must hold
valueArrayThe value to check, passed to the check that comes back
ReturnsArrayThe value
Inputs and return value of Security.Array
//  Check whether the variable is an array
Security.Array()(items)

//  Check whether the variable is an array of integers
Security.Array(Security.Integer)(counts)

//  Check whether the variable is an array of optional integers
Security.Array(Security.OptionalInteger)(scores)

//  Check whether the variable is an array of arrays of numbers
Security.Array(Security.Array(Security.Number))(grid)

//  Check whether the variable is an array of players
Security.Array('Game.Player')(players)
Checking whether a variable is an array of something

Security.Dictionary

Makes a check for an object used as a dictionary, holding every value in it to a check or a type name. The keys can be anything.

InputTypeDescription
ofCheck or type nameThe check for each value, or the type name each value's _is field must hold
valueObjectThe value to check, passed to the check that comes back
ReturnsObjectThe value
Inputs and return value of Security.Dictionary
//  Check whether the variable is an object of numbers
Security.Dictionary(Security.Number)(scores)

//  Check whether the variable is an object of players
Security.Dictionary('Game.Player')(playersByName)
Checking whether a variable is a dictionary of something

Security.Object

Makes a check for an object. A function passes too, since functions are often used as objects, but an array, null or undefined fails. Given a type name, it also checks the object's _is field. Given a function, it hands the object to it.

InputTypeDescription
ofType name or check, optionalThe type name the _is field must hold, or a check to hand the object to
valueObjectThe value to check, passed to the check that comes back
ReturnsObjectThe value, or what the of check returns when it's a function
Inputs and return value of Security.Object
//  Check whether the variable is an object
Security.Object()(options)

//  Check whether the variable is a player
Security.Object('Game.Player')(player)

//  Check whether the variable is an object with a name
Security.Object(value => Security.String(value.name))(player)
Checking whether a variable is an object

Security.Is

Fails unless the value is an object of a type. It's Security.Object(type)(value) in a single call, with the value first.

InputTypeDescription
valueObjectThe value to check
typeType name or checkThe type name the _is field must hold, or a check to hand the object to
ReturnsObjectThe value
Inputs and return value of Security.Is
//  Check whether the variable is a player
Security.Is(player, 'Game.Player')
Checking whether a variable is an object of a type

Security.OneField

Fails unless the value has exactly one field, ignoring those that start with an underscore, whose name is one of the options and whose contents pass that option's check. An option can be a type name, a check, or undefined for a field whose contents aren't checked. An object with none of the fields fails with a message listing them, like Expected one of the options circle, square; got none.

InputTypeDescription
valueObjectThe value to check
optionsObjectEach field name allowed, with its type name, check or undefined
ReturnsObjectThe value
Inputs and return value of Security.OneField
//  Check whether the shape is a circle or a square
Security.OneField(shape, {
    circle: 'Shapes.Circle',
    square: 'Shapes.Square',
})

//  Check whether the size is a number of pixels, or automatic
Security.OneField(size, {
    pixels: Security.Integer,
    automatic: undefined,
})
Checking whether a variable is one of several options

Security.Validate

Makes a check from either a check function or a type name. It's how Security.Array and Security.Dictionary accept both, and it does the same for a check of your own.

InputTypeDescription
validationCheck or type nameA check function, or the type name an object's _is field must hold
valueAnyThe value to check, passed to the check that comes back
ReturnsAnyWhat the check function returns, or the value for a type name
Inputs and return value of Security.Validate
//  Check a value with a check function
Security.Validate(Security.String)(name)

//  Check a value with a type name
Security.Validate('Game.Player')(player)
Checking a value with whichever kind of check was given

Security.Type

A placeholder for a check by type name. For now it checks that the type name is a string, and accepts any value.

InputTypeDescription
typeStringStringThe type name
valueAnyThe value to check, passed to the check that comes back
ReturnsNothing
Inputs and return value of Security.Type
//  Name the type expected; any value passes for now
Security.Type('Game.Player')(player)
Naming the type a variable should be

Security.Optional

Makes a check that lets undefined through and holds anything else to a check or a type name. null counts as a value, so it's checked.

InputTypeDescription
ofCheck or type nameThe check for a value that's there, or the type name its _is field must hold
valueAnyThe value to check, passed to the check that comes back
ReturnsAnyThe value, or what the check returns when it's a function, which for the basic checks is the value too
Inputs and return value of Security.Optional
//  Check whether the variable is a string, if it's there
Security.Optional(Security.String)(optionalCharacter)

//  Check whether the variable is an integer from 1 to 10, if it's there
Security.Optional(level => Security.Integer(level, 1, 10))(optionalLevel)

//  Check whether the variable is a player, if it's there
Security.Optional('Game.Player')(optionalPlayer)
Checking whether a variable is something, if it's there

Security.OptionalString

Fails unless the value is undefined or a string. It's Security.Optional(Security.String)(value).

InputTypeDescription
valueString, optionalThe value to check
ReturnsString or undefinedThe value
Inputs and return value of Security.OptionalString
//  Check whether the variable is a string, if it's there
Security.OptionalString(optionalTitle)
Checking whether a variable is a string, if it's there

Security.OptionalNumber

Fails unless the value is undefined or a number. It takes no limits; for those, use Security.Optional with a function.

InputTypeDescription
valueNumber, optionalThe value to check
ReturnsNumber or undefinedThe value
Inputs and return value of Security.OptionalNumber
//  Check whether the variable is a number, if it's there
Security.OptionalNumber(optionalSpeed)
Checking whether a variable is a number, if it's there

Security.OptionalInteger

Fails unless the value is undefined or a whole number. Like OptionalNumber, it takes no limits.

InputTypeDescription
valueInteger, optionalThe value to check
ReturnsNumber or undefinedThe value
Inputs and return value of Security.OptionalInteger
//  Check whether the variable is an integer, if it's there
Security.OptionalInteger(optionalCount)

//  Check whether the variable is an array of optional integers
Security.Array(Security.OptionalInteger)(scores)
Checking whether a variable is an integer, if it's there

Security.OptionalCallback

Fails unless the value is undefined or a function.

InputTypeDescription
valueFunction, optionalThe value to check
ReturnsFunction or undefinedThe value
Inputs and return value of Security.OptionalCallback
//  Check whether the variable is a function, if it's there
Security.OptionalCallback(optionalOnDone)
Checking whether a variable is a function, if it's there

Security.OptionalArray

Makes a check for a value that's either undefined or an array, with its elements checked as Security.Array does.

InputTypeDescription
ofCheck or type name, optionalThe check for each element, or the type name each element's _is field must hold
valueArray, optionalThe value to check, passed to the check that comes back
ReturnsArray or undefinedThe value
Inputs and return value of Security.OptionalArray
//  Check whether the variable is an array of strings, if it's there
Security.OptionalArray(Security.String)(optionalTags)

//  Check whether the variable is an array of players, if it's there
Security.OptionalArray('Game.Player')(optionalPlayers)
Checking whether a variable is an array, if it's there

Security.OptionalObject

Makes a check for a value that's either undefined or an object, checked as Security.Object does.

InputTypeDescription
ofType name or check, optionalThe type name the _is field must hold, or a check to hand the object to
valueObject, optionalThe value to check, passed to the check that comes back
ReturnsObject or undefinedThe value, or what the of check returns when it's a function
Inputs and return value of Security.OptionalObject
//  Check whether the variable is an object, if it's there
Security.OptionalObject()(optionalSettings)

//  Check whether the variable is a player, if it's there
Security.OptionalObject('Game.Player')(optionalPlayer)
Checking whether a variable is an object, if it's there

Security.OptionalIs

Fails unless the value is undefined or of a type. It's Security.Optional(type)(value) in a single call, with the value first.

InputTypeDescription
valueObject, optionalThe value to check
typeType name or checkThe type name the _is field must hold, or a check for the value
ReturnsAnyThe value, or what the check returns when it's a function
Inputs and return value of Security.OptionalIs
//  Check whether the variable is a player, if it's there
Security.OptionalIs(optionalPlayer, 'Game.Player')
Checking whether a variable is of a type, if it's there

Security.Equal

Fails unless two values write out the same, with the keys of every object sorted, and shows them side by side from the first difference, as in Comparing two values.

InputTypeDescription
aAnyThe value found, shown on the left
bAnyThe value expected, shown on the right
messageString, optionalWhat's being compared, put at the start of the failure
ReturnsNothing
Inputs and return value of Security.Equal
//  Check that the saved player matches the one expected
Security.Equal(saved, expected, 'Saved player')

//  The order of the keys doesn't matter, so this passes
Security.Equal({ a: 1, b: 2 }, { b: 2, a: 1 })
Comparing two values in a test

Security.Enter

Runs a callback with a description of what it's doing, which any failure inside adds to its message. The description is taken off again when the callback finishes, even if it throws.

InputTypeDescription
descriptionStringWhat the callback is doing
callbackFunctionThe code to run
ReturnsAnyWhatever the callback returns
Inputs and return value of Security.Enter
//  Read the map, saying so if anything fails
const map = Security.Enter('reading the map', () => {
    return Level.ReadMap(data)
})

//  Describe each item in a loop
items.forEach( (item, i) => {
    Security.Enter(`saving item ${i}`, () => {
        Store.Save(item)
    })
})
Describing what's being done while a callback runs

Security.Fail

Throws an error with a message, followed by a while: line for each description from Security.Enter. Every check fails through it, and it starts with a debugger statement, so the developer tools stop on the failure.

InputTypeDescription
msgStringWhat went wrong
ReturnsNeverIt always throws
Inputs and return value of Security.Fail
//  Fail when a case isn't handled
switch( shape.kind ) {
    case 'circle': return Shapes.CircleArea(shape)
    case 'square': return Shapes.SquareArea(shape)
    default: Security.Fail(`Unknown shape: ${shape.kind}`)
}
Failing with a message of your own

Source code

The module is one file with no dependencies, so it can go into any Node project as node_modules/Security/index.js and be loaded with require('Security').

The Security module

  • index.js JavaScript · 601 lines · every check, the comparison and the record of what was going on, each with a doc comment

See also