Patterns

Three of the most common shapes in TypeScript are arrays of things, objects that contain objects, and values that may be missing. This section shows how each one converts, and what TJS checks once it has. Each pattern below links to its example page, where the code runs in the page and you can edit it.

Converting, and opting in

The converter, fromTS (on the command line, tjs convert --emit-tjs file.ts), reads your TypeScript and writes TJS, which the TJS compiler then turns into JavaScript. The example pages do both steps for you. Converting a Project covers doing it to your own code.

Converted code keeps JavaScript's behaviour by default, so converting a working program does not change what it does: the types are carried along and nothing checks them. To turn them into checks, put this comment at the top of the TypeScript file, before converting it:

/* @tjs TjsStrict */

function sum(numbers: number[]): number {
  return numbers.reduce((a, b) => a + b, 0)
}

With the line, every parameter is checked at run time, on every call, before the function body runs. Return types are not checked: : number converts to :! number, a type-only return that TJS records for documentation.

A failed check does not throw. The function returns a MonadicError in place of its result, an error value that names the parameter, the type it expected and what arrived. Runtime Validation covers what to do with one.

Every example in this section runs with the line, and the bad call is already in the code, so you see the check fire. Array Operations also shows the same functions without it, and the other two say what prints when you delete it.

TypeScript Converts to What TjsStrict checks
number[] [number], an array of number every element, not only "is an array"
interface Person { … } Type Person { example: … } the whole shape, nested types included
string | null the same union a string or null; undefined is refused
type Status = 'a' | 'b' Union Status 'Status' … one of the listed strings

Arrays

Array Operations

/* @tjs TjsStrict */

function sum(numbers: number[]): number {
  return numbers.reduce((a, b) => a + b, 0)
}

const scores: number[] = JSON.parse('[1, "2", 3]')
console.log(sum(scores).message)

number[] converts to [number]. In TJS an array with one item in it describes an array of that item, so [number] means "an array of numbers", not a one-element tuple. Each element is checked, so one string in a list of numbers is caught, and the error names its index:

Expected number for '<source>:3:sum.numbers[1]', got string

The path is the source, the line where sum is declared, then the parameter and the index of the first bad element. Without the TjsStrict line the same call returns the string '123', because reduce turns + into string concatenation as soon as it meets '2'.

Checking every element costs time proportional to the array's length, on every call. Check at the boundary where data arrives; once a file is native TJS you can mark an inner function's parameter with ! to skip its check. Array Operations shows how, and the performance guide has measurements.

A TypeScript tuple, [number, string], converts to [0.0, '']. TJS reads that as an array whose items may be any of those types, not as a tuple: length and position are not checked.

Nested objects

Nested Objects

Each interface becomes a TJS Type. A TJS type is described by an example value: '' means "a string" and 0.0 means "a number". The converter picks the example from your TypeScript type, so you never write one yourself. An interface that uses another one by name keeps the name:

interface Address {
  city: string
}

interface Person {
  name: string
  address: Address
}

function city(person: Person): string {
  return person.address.city
}

The two interfaces become

Type Address {
  example: { city: '' }
}

Type Person {
  example: { name: '', address: Address }
}

At run time address: Address refers to the Address type itself, not a copy of its shape, so checking a Person checks its address with Address. Because it is a reference, a type can name itself (next: ListNode | null) or a type declared later in the file, as in TypeScript.

The error names the outer type that failed, not the member inside it. A Person whose address.city is 1 is rejected with:

Expected Person for '…city.person', got object

That is how the check reports today: it is deep, the message is not. The path tells you which parameter to look at; finding the bad member inside it is up to you. Once the file is native TJS, Address.check(person.address) returns true or false, so you can test the inner value on its own.

Nullable values

Union Types

/* @tjs TjsStrict */

function greet(name: string | null): string {
  return name === null ? 'nobody' : 'hi ' + name
}

greet(null) // 'nobody'
greet(undefined as any) // a MonadicError

With strictNullChecks, TypeScript already keeps null and undefined apart, but only at compile time, and only for values tsc can see. TJS checks it when the function is called: string | null accepts a string or null and rejects undefined, including an undefined that arrived from JSON or through a cast.

Expected string for '<source>:3:greet.name', got undefined

To accept a string, null, or a missing argument, write name?: string | null. It converts to name: string | null | undefined, which accepts all three.

Named literal unions (type Status = 'active' | 'inactive') and unions of types (number | string) are checked too. Inline literal unions and discriminated unions are not: the first degrades to an unchecked parameter with a TODO comment, and the second converts to a Type that accepts any value. Union Types shows how to write them so they are checked.

Elsewhere

Optional interface members (address?: Address) are covered at the end of Nested Objects. Optional parameters and defaults have their own page, Optional Parameters. Generics, async functions and classes are in Advanced.