Safety & Validation

In TJS, a typed parameter is checked every time the function is called, not only when the code is compiled. A wrong argument produces an error value that says what was expected and what arrived. This page shows what is checked, how to check return values too, and how to turn the checks off where you have a reason to.

The problem

JavaScript functions accept whatever they are given:

function greet(name) { return `Hello, ${name}!` }
greet(42) // "Hello, 42!" — no error, just wrong

TypeScript catches this at compile time, but its types are erased (removed when the code becomes JavaScript), so nothing checks the value that actually arrives while the program runs: data from a network request, a file, or a caller written in plain JavaScript.

Inputs are checked

In function sayHello(name: 'World'): 'Hello, World!', the value after each colon is an example, not a type and not a fixed value. name: 'World' says name is a string, with 'World' as a sample. The return example 'Hello, World!' is a worked example: when TJS turns the file into JavaScript (transpile time), it calls sayHello('World') and checks that the result equals 'Hello, World!'. That check is the signature test. See Declarations for the full rules.

How strict a numeric check is depends on how the example is written:

Example Accepts
0 any integer
0.0 any number
+0 a non-negative integer

The test '…' { } blocks below are TJS's inline tests: they run with the example and report pass or fail (see Inline Tests). isMonadicError(x) is true for an error TJS itself returned.

// --- Input validation catches bad callers ---

function sayHello(name: 'World'): 'Hello, World!' {
  return `Hello, ${name}!`
}

test 'valid input works normally' {
  expect(sayHello('Alice')).toBe('Hello, Alice!')
}

test 'wrong type returns an error, not garbage' {
  // Not "Hello, 42!": an error value
  expect(isMonadicError(sayHello(42))).toBe(true)
}

test 'null returns an error' {
  expect(isMonadicError(sayHello(null))).toBe(true)
}

// --- The example decides how strict the check is ---

function setAge(age: +0): +0 {
  return age
}

test 'non-negative integer accepted' {
  expect(setAge(25)).toBe(25)
}

test 'negative rejected' {
  expect(isMonadicError(setAge(-1))).toBe(true)
}

test 'float rejected (an age is an integer)' {
  expect(isMonadicError(setAge(25.5))).toBe(true)
}

// --- Errors pass through typed functions ---

function cleanName(name: ''): '' {
  return name.trim()
}

function upperCase(name: ''): '' {
  return name.toUpperCase()
}

test 'an error travels to the end of the chain' {
  // cleanName(42) returns an error; upperCase receives it and returns it unchanged
  expect(isMonadicError(upperCase(cleanName(42)))).toBe(true)
  expect(upperCase(cleanName(' alice '))).toBe('ALICE')
}

console.log('sayHello("World"):', sayHello('World'))
console.log('sayHello(42):     ', sayHello(42).message)
console.log('setAge(25.5):     ', setAge(25.5).message)
console.log('chain:            ', upperCase(cleanName(42)).message)
// → sayHello("World"): Hello, World!
// → sayHello(42):      Expected string for '…:3:sayHello.name', got number
// → setAge(25.5):      Expected non-negative-integer for '…:22:setAge.age', got number
// → chain:             Expected string for '…:40:cleanName.name', got number

What to notice

A bad argument produces a value, not an exception. sayHello(42) neither prints Hello, 42! nor throws. It returns a MonadicError: a subclass of Error that is returned rather than thrown, so instanceof Error is true for it as well. Its message names the expected type, the actual type, and where the check failed (Expected string for '…:3:sayHello.name', got number; the … stands for the file name, and 3 is the line). Your program keeps running, and the error is something you can inspect, log, or return.

The example decides how strict the check is. age: +0 accepts 25 and refuses both -1 and 25.5. You get "non-negative integer" without writing a validator, because that is what the example +0 means.

Errors pass through typed functions. When a function with typed parameters receives a MonadicError as an argument, it does not run its body; it returns that error. So upperCase(cleanName(42)) needs no check between the two calls, and the message at the end still names cleanName, where the failure happened. Error Handling covers this in depth, including where in a program to deal with the error.

Turning the checks off

! on one function

A ! as the first thing inside the parentheses, before the first parameter, tells TJS to trust the callers of that function. It turns off the checks for every parameter, not only the one it sits next to:

function fastAdd(! a: 0, b: 0): 0 {
  return a + b
}

test 'an unsafe function skips its checks' {
  expect(fastAdd(3, 4)).toBe(7)
  expect(fastAdd('a', 'b')).toBe('ab') // no error: validation is off
}

console.log("fastAdd('a', 'b'):", fastAdd('a', 'b'))
// → fastAdd('a', 'b'): ab

The signature is still recorded and still used for documentation and autocomplete, and the signature test fastAdd(0, 0) still runs at transpile time; only the runtime check is gone. Use it on hot internal functions whose inputs were already checked at the boundary, never on a function that receives outside data.

safety none for a whole file

A safety line as the first line of a file sets the default for every function in it. safety inputs is the default and what every other example on this page uses, so you never need to write it. safety none turns off every runtime check in the file, while the signatures stay available for documentation, autocomplete and signature tests. Each example on this page is its own file, so safety none here affects only this block:

safety none

function greet(name: 'World') {
  return `Hello, ${name}!`
}

console.log(greet(42)) // "Hello, 42!": nothing is checked in this file

Reach for safety none only when a file is a measured hot spot and everything that calls it already validates its data. safety all also exists, but it currently checks the same things as safety inputs; use :? (below) to check returns. Likewise a ? before the parameters (function f(? a: 0)) is accepted and has no effect. See the syntax reference, under "The safety directive".

Checking returns as well

Input checks catch bad callers. To catch your own bugs, check what a function returns with :?. Take function add(a: 2, b: 3): 5. With :, TJS calls add(2, 3) at transpile time and checks the result equals 5. :! skips that test, for when the example only describes the type and is not a real answer. :? runs the test and also checks every value the function returns while the program runs:

Return form At transpile time At runtime
: 0 runs the signature example as a test nothing
:! 0 nothing (declares the type only) nothing
:? 0 runs the signature example as a test checks every value returned
function average(xs: [0.0]):? 0.0 {
  if (xs.length == 0) return 'no data' // a bug: a string where a number belongs
  return xs.reduce((a, b) => a + b, 0) / xs.length
}

console.log('average([2, 4]):', average([2, 4]))

const r = average([])
console.log('average([]):    ', r instanceof Error ? r.message : r)
// → average([2, 4]): 3
// → average([]):     Expected number for '…:1:average:return', got string

The bad path is caught where it happens, with the function's name in the message, rather than later, when some other function does arithmetic on 'no data' and produces NaN far from the cause.

Class methods and constructors are checked too

Typed parameters on a constructor or a method validate exactly as a function's do, so a class cannot be built from, or fed, the wrong kind of value. TJS classes are called without new (see Better Classes):

class Account {
  constructor(owner: 'Ada', balance: 0.0) {
    this.owner = owner
    this.balance = balance
  }

  deposit(amount: 0.0) {
    this.balance += amount
    return this.balance
  }
}

const acct = Account('Ada', 10) // no `new` needed in TJS
console.log('deposit(5):    ', acct.deposit(5))

const bad = acct.deposit('5')
console.log("deposit('5'):  ", bad instanceof Error ? bad.message : bad)

const nobody = Account(42, 0)
console.log('Account(42, 0):', nobody instanceof Error ? nobody.message : nobody)
// → deposit(5):     15
// → deposit('5'):   Expected number for '…:7:Account.deposit.amount', got string
// → Account(42, 0): Expected string for '…:2:Account.constructor.owner', got number

The constructor returns the error instead of an object, so nobody is a MonadicError.

When code you call throws

Type checks cover the values you pass. Code you call can still throw: JSON.parse throws on malformed text. TJS lets you write a try block with no catch, which turns such an exception into a returned error; Error Handling covers it.

Gotchas