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
}
}
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 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)
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')
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)
}
}
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)
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
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,
}
}
}
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',
})
{ 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')
Saved player; Expected values to be equal;
------------------------------------------------------------------------------
------------------------------------------------------------------------------
|{ | {
|level:1, | level:1,
|name:"Bob", | name:"Bob",
------------------ 8< ------------------------------------- 8< ------------------------------------- 8< -------------------
|score:10 | score:12
|} | }
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)
})
})
A failure anywhere inside adds the descriptions to its message:
Error: Expected number; got undefined: undefined
while: loading level 3
while: reading the map
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.Typeis a placeholder: it checks that its type name is a string and accepts any value.Security.Check,Security.EqualandSecurity.Typereturn nothing. Every other check returns the value, or what the check it was given returns, so any of them can sit inside an expression.NaNfailsSecurity.NumberandSecurity.Integer, even though its type isnumber, andnullfailsSecurity.Object, even though its type isobject.- The module has no dependencies. It only calls
requireto look up a type by name when an object's_isfield holds a function rather than a string:'Game.Player'loads theGamemodule and walks down toPlayer, and the two functions have to match. - The first line raises
Error.stackTraceLimitto 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.
- Conditions:
Check,Defined,Any - Single values:
String,Number,Integer,Boolean,Callback,CallbackOrString,HexString,Hex32,Hex64,ArrayBuffer,Buffer - Arrays and objects:
Array,Dictionary,Object,Is,OneField,Validate,Type - Optional values:
Optional,OptionalString,OptionalNumber,OptionalInteger,OptionalCallback,OptionalArray,OptionalObject,OptionalIs - Tests and errors:
Equal,Enter,Fail
Security.Check
Fails unless a condition is true. It covers anything the other checks don't.
| Input | Type | Description |
|---|---|---|
boolean | Any | The condition |
optionalMessage | String, optional | The message to fail with, instead of Check failed |
| Returns | Nothing |
// 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)
Security.Defined
Fails if the value is null or undefined. Anything else passes, including 0, false and an empty string.
| Input | Type | Description |
|---|---|---|
value | Any | The value to check |
optionalMessage | String, optional | The message to fail with, instead of Value is not defined |
| Returns | Any | The value |
// 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')
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.
| Input | Type | Description |
|---|---|---|
value | Any | The value |
| Returns | Any | The value |
const Cache = {
Set: (cache, key, value) => {
Security.Object()(cache)
Security.String(key)
Security.Any(value)
cache[key] = value
}
}
Security.String
Fails unless the value is a string, with a message like Expected string; got number: 3.
| Input | Type | Description |
|---|---|---|
value | String | The value to check |
| Returns | String | The value |
// Check whether the variable is a string
Security.String(name)
// Check the title and keep it
const title = Security.String(input.title)
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.
| Input | Type | Description |
|---|---|---|
value | Number | The value to check |
optionalMinValue | Number, optional | The smallest value allowed |
optionalMaxValue | Number, optional | The largest value allowed |
| Returns | Number | The value |
// 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)
Security.Integer
Fails unless the value is a whole number, and within the limits if there are any.
| Input | Type | Description |
|---|---|---|
value | Number | The value to check |
optionalMinValue | Integer, optional | The smallest value allowed |
optionalMaxValue | Integer, optional | The largest value allowed |
| Returns | Number | The value |
// 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)
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.
| Input | Type | Description |
|---|---|---|
value | Boolean | The value to check |
| Returns | Boolean | The value |
// Check whether the variable is true or false
Security.Boolean(muted)
Security.Callback
Fails unless the value is a function, with the message Expected function.
| Input | Type | Description |
|---|---|---|
value | Function | The value to check |
| Returns | Function | The value |
// Check whether the variable is a function
Security.Callback(onDone)
Security.CallbackOrString
Fails unless the value is a function or a string, with a message like Expected callback or string; got number.
| Input | Type | Description |
|---|---|---|
input | Function or string | The value to check |
| Returns | Function or string | The value |
// Check whether the label is fixed text, or a function that makes it
Security.CallbackOrString(label)
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.
| Input | Type | Description |
|---|---|---|
value | String | The value to check |
expectedLength | Integer, optional | The number of digits it must have |
| Returns | String | The value |
// 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)
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"
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).
| Input | Type | Description |
|---|---|---|
value | String | The value to check |
| Returns | String | The value |
// Check whether the variable is an MD5 hash
Security.Hex32(hash)
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).
| Input | Type | Description |
|---|---|---|
value | String | The value to check |
| Returns | String | The value |
// 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)
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.
| Input | Type | Description |
|---|---|---|
a | ArrayBuffer | The value to check |
| Returns | ArrayBuffer | The value |
// Check whether the variable is an ArrayBuffer
Security.ArrayBuffer(data)
// Check the buffer behind a Uint8Array
Security.ArrayBuffer(bytes.buffer)
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.
| Input | Type | Description |
|---|---|---|
buffer | ArrayBuffer or Buffer | The value to check |
| Returns | ArrayBuffer or Buffer | The value |
// Check whether the variable is a buffer
Security.Buffer(data)
// Check the buffer behind a Uint8Array
Security.Buffer(bytes.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.
| Input | Type | Description |
|---|---|---|
of | Check or type name, optional | The check for each element, or the type name each element's _is field must hold |
value | Array | The value to check, passed to the check that comes back |
| Returns | Array | The value |
// 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)
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.
| Input | Type | Description |
|---|---|---|
of | Check or type name | The check for each value, or the type name each value's _is field must hold |
value | Object | The value to check, passed to the check that comes back |
| Returns | Object | The value |
// 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)
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.
| Input | Type | Description |
|---|---|---|
of | Type name or check, optional | The type name the _is field must hold, or a check to hand the object to |
value | Object | The value to check, passed to the check that comes back |
| Returns | Object | The value, or what the of check returns when it's a function |
// 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)
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.
| Input | Type | Description |
|---|---|---|
value | Object | The value to check |
type | Type name or check | The type name the _is field must hold, or a check to hand the object to |
| Returns | Object | The value |
// Check whether the variable is a player
Security.Is(player, 'Game.Player')
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.
| Input | Type | Description |
|---|---|---|
value | Object | The value to check |
options | Object | Each field name allowed, with its type name, check or undefined |
| Returns | Object | The value |
// 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,
})
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.
| Input | Type | Description |
|---|---|---|
validation | Check or type name | A check function, or the type name an object's _is field must hold |
value | Any | The value to check, passed to the check that comes back |
| Returns | Any | What the check function returns, or the value for a type name |
// Check a value with a check function
Security.Validate(Security.String)(name)
// Check a value with a type name
Security.Validate('Game.Player')(player)
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.
| Input | Type | Description |
|---|---|---|
typeString | String | The type name |
value | Any | The value to check, passed to the check that comes back |
| Returns | Nothing |
// Name the type expected; any value passes for now
Security.Type('Game.Player')(player)
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.
| Input | Type | Description |
|---|---|---|
of | Check or type name | The check for a value that's there, or the type name its _is field must hold |
value | Any | The value to check, passed to the check that comes back |
| Returns | Any | The value, or what the check returns when it's a function, which for the basic checks is the value too |
// 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)
Security.OptionalString
Fails unless the value is undefined or a string. It's Security.Optional(Security.String)(value).
| Input | Type | Description |
|---|---|---|
value | String, optional | The value to check |
| Returns | String or undefined | The value |
// Check whether the variable is a string, if it's there
Security.OptionalString(optionalTitle)
Security.OptionalNumber
Fails unless the value is undefined or a number. It takes no limits; for those, use Security.Optional with a function.
| Input | Type | Description |
|---|---|---|
value | Number, optional | The value to check |
| Returns | Number or undefined | The value |
// Check whether the variable is a number, if it's there
Security.OptionalNumber(optionalSpeed)
Security.OptionalInteger
Fails unless the value is undefined or a whole number. Like OptionalNumber, it takes no limits.
| Input | Type | Description |
|---|---|---|
value | Integer, optional | The value to check |
| Returns | Number or undefined | The value |
// 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)
Security.OptionalCallback
Fails unless the value is undefined or a function.
| Input | Type | Description |
|---|---|---|
value | Function, optional | The value to check |
| Returns | Function or undefined | The value |
// Check whether the variable is a function, if it's there
Security.OptionalCallback(optionalOnDone)
Security.OptionalArray
Makes a check for a value that's either undefined or an array, with its elements checked as Security.Array does.
| Input | Type | Description |
|---|---|---|
of | Check or type name, optional | The check for each element, or the type name each element's _is field must hold |
value | Array, optional | The value to check, passed to the check that comes back |
| Returns | Array or undefined | The value |
// 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)
Security.OptionalObject
Makes a check for a value that's either undefined or an object, checked as Security.Object does.
| Input | Type | Description |
|---|---|---|
of | Type name or check, optional | The type name the _is field must hold, or a check to hand the object to |
value | Object, optional | The value to check, passed to the check that comes back |
| Returns | Object or undefined | The value, or what the of check returns when it's a function |
// 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)
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.
| Input | Type | Description |
|---|---|---|
value | Object, optional | The value to check |
type | Type name or check | The type name the _is field must hold, or a check for the value |
| Returns | Any | The value, or what the check returns when it's a function |
// Check whether the variable is a player, if it's there
Security.OptionalIs(optionalPlayer, 'Game.Player')
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.
| Input | Type | Description |
|---|---|---|
a | Any | The value found, shown on the left |
b | Any | The value expected, shown on the right |
message | String, optional | What's being compared, put at the start of the failure |
| Returns | Nothing |
// 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 })
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.
| Input | Type | Description |
|---|---|---|
description | String | What the callback is doing |
callback | Function | The code to run |
| Returns | Any | Whatever the callback returns |
// 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)
})
})
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.
| Input | Type | Description |
|---|---|---|
msg | String | What went wrong |
| Returns | Never | It always throws |
// 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}`)
}
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
- Curried functions in JavaScript -Functions called with only some of their arguments, which is how the array and object checks take their types
- How I organise Node modules - The conventions every module follows, including checking every argument with Security
- Composable typechecking in JavaScript - Building bigger types out of checks, and why the checks are written this way
- Type safety at the boundaries - A cut-down TypeScript version of these checks, for decoding API data into immutable objects