TabScript is an alternate syntax for TypeScript that replaces braces with indentation and introduces shorthand operators while maintaining full TypeScript compatibility. The compiler outputs clean TypeScript or JavaScript.
Let's start with a complete example that showcases TabScript's clean syntax:
tabscript 2.0
interface Task
title: string
status: "done" or "pending"
priority: number
# Arrow function with := (const)
filterTasks := |tasks: Task[], status: "done" or "pending"|
tasks.filter.. |t| t.status == status and t.priority > 0
# Single expression functions
getHighPriority := |tasks: Task[]|
tasks.filter.. |t| t.priority >= 8
# Named function
function printTaskStats|tasks: Task[]|
completed := filterTasks.. tasks "done"
pending := filterTasks.. tasks "pending"
# for-of with : declares a const
for task: of getHighPriority(pending)
console.log("HIGH PRIORITY:", task.title)
if completed.length > 0
console.log(`Completed ${completed.length} tasks!`)
A few things to note:
:= for const and ::= for let.== means ===).Variable declarations use colons: a single : for const and a double :: for let.
tabscript 2.0
# One colon means const
x : number = 3
z := 42
# Two colons means let
y :: string = "hello"
w ::= 42
# Declaration without initial value
arr : number[]
# Union types (use 'or' instead of |)
value : string or undefined
Functions use || to wrap parameters instead of (). For arrow functions, you can omit braces when returning an expression.
Note that we're leaving out the required tabscript 2.0 header in the following examples for brevity.
# Arrow functions
add := |a, b| a + b
double := |x: number| x * 2
# Async arrow function
fetch := async |url| await loadData(url)
# Named function with single expression
function greet|name| `Hi ${name}`
# Named function with block body
function calculate|a: number, b: number|
result := a + b
return result
# Generic function
identity := <T>|x: T| x
Use .. to call functions with space-separated arguments or one argument per line for cleaner syntax.
# Regular call (traditional syntax still works)
result := func(a, b)
# Call with .. and space-separated args
result := func.. a b
# Call with .. and indented args
result := func..
a
b
# Passing in an anonymous function as argument
processData.. options |item|
item.value *= 2
All control structures use indentation instead of braces.
# If statement (single line)
if x > 0 console.log("positive")
# If statement (block body)
if x > 0
console.log("positive")
x++
# If-else
if x > 0
console.log("positive")
else
console.log("not positive")
# While loop
while i < 10
i++
# For-of loop with type-inferred constant
for item: of array
console.log(item)
# For-in loop
for key: in obj
console.log(key, obj[key])
# C-style for loop with let
for i ::= 0; i < 10; i++
console.log(i)
# Switch (values don't need 'case' keyword)
switch day
1
console.log("Monday")
2
console.log("Tuesday")
*
console.log("Other day")
# Try-catch
try
riskyOperation()
catch error
console.log(error)
# Or without the catch, and on a single line
try riskyOperation()
TabScript uses and and or for logical operators instead of && and ||.
if x > 0 and y > 0
console.log("both positive")
if x == 0 or y == 0
console.log("at least one zero")
TabScript uses == and != for strict equality (like TypeScript's === and !==). For loose equality, use =~ and !~.
# Strict equality by default
if x == y
console.log("equal")
if x != y
console.log("not equal")
# Explicit loose equality
if x =~ y
console.log("loosely equal")
Test if an expression is neither null nor undefined by suffixing it with ?.
if getValue()?
console.log("has value")
Binary operators and modulo use verbose names with a % prefix. They should be relatively uncommon, so this makes their use more explicit and frees up symbols for other, more frequently used, constructs.
# Bitwise operations
result := x %bit_or y
result := x %bit_and y
result := x %bit_xor y
result := %bit_not x
# Bit shifts
result := x %shift_left 2
result := x %shift_right 2
result := x %unsigned_shift_right 2
# Modulo
console.log(5 %mod 3, "equals 2")
Classes use indentation-based syntax. Methods need || even when they have no parameters.
# Basic class with properties
class Person
name: string
age: number
# Constructor with parameter properties
class Person
constructor|
public name: string
private age: number
| ;
# Methods (|| means no parameters)
class Person
greet||
return "Hello"
setAge|age: number|
this.age = age
# Getters and setters
class Person
get name||
return this._name
set name|value|
this._name = value
# Inheritance
class Dog extends Animal implements Pet
makeSound||
console.log("Woof!")
# Generic class
class Box<T>
value: T
constructor|value: T|
this.value = value
TypeScript's type system is fully supported with TabScript syntax.
# Interface with properties
interface User
name: string
email: string
age: number
# Interface with methods
interface Service
start||: void
stop||: void
getData|id: string|: Data
# Type aliases
type ID = string or number
type Point = {x: number, y: number}
# Generic type with union
type Result<T> = {success: true, data: T} or {success: false, error: string}
# Function types use pipes
type Handler = |event: Event|: void
type Mapper<T, U> = |input: T|: U
Enums work the same as in TypeScript, with indentation instead of braces.
# Basic enum
enum Color
Red
Green
Blue
# Enum with explicit values
enum Status
Active = 1
Inactive = 0
# Traditional brace syntax also works
enum Direction { Up, Down, Left, Right }
TabScript's plugin system lets you extend the language with custom syntax tailored to your domain. Plugins hook into the parser to recognize new syntax patterns and emit custom output while maintaining full IDE support.
The transpiler is lexer-less and single-pass — it reads input and emits output simultaneously without building an AST. This makes it fast and simple but best suited for transformations that map cleanly to underlying TypeScript constructs.
Import plugins using the import plugin statement:
# @file: my-plugin.tab
export default function||;
tabscript 2.0
import plugin "./my-plugin.tab"
You can pass options to plugins using an object literal:
# @file: options-demo.tab
export default function|parser, options, pluginOptions|
console.log(pluginOptions)
tabscript 2.0
import plugin "./options-demo.tab" {function: "UI", debug: true}
Plugins can be loaded at any point in the file and take effect immediately. They can be written in TabScript (.tab) or JavaScript (.js).
Plugins receive the Parser instance and can directly modify its parse* methods. The parser uses methods like parseStatement, parseExpression, and parseType to process different parts of the syntax. Plugins can replace or wrap these methods to add new syntax.
Here's a plugin that adds an @log decorator for automatic function call logging:
# @file: log-plugin.tab
tabscript 2.0
import type {Parser, State, Options} from "tabscript"
export default function|p: Parser, options: Options|
IDENTIFIER := p.pattern.. /[a-zA-Z_$][0-9a-zA-Z_$]*/ "identifier"
# Keep reference to original parseStatement
origParseStatement := p.parseStatement.bind(p)
# Replace parseStatement to handle @log before other statements
p.parseStatement = |s: State|
if !s.read.. '@log'
return origParseStatement(s)
# Parse: @log name := |args| body
name := s.must.. s.read.. IDENTIFIER
s.must.. s.read.. ':'
isLet := !!s.read.. ':'
s.emit.. (isLet ? 'let ' : 'const ') + name
s.must.. s.read.. '='
# Wrap function with logging
s.emit.. '=('
s.must.. p.parseFuncParams(s)
s.emit.. '=>{console.log(' + JSON.stringify(name) + ',...arguments);return('
s.must.. p.parseExpression(s)
s.emit.. ');})'
return true
Usage:
tabscript 2.0
import plugin "./log-plugin.tab"
@log add := |a: number, b: number| a + b
result := add(1, 2) # Logs: "add" 1 2
Plugins export a default function that receives three arguments:
parser - The Parser instance with all parse* methodsoptions - Global transpiler options (includes js flag)pluginOptions - Options passed in the object literal after the plugin pathTo add custom syntax, save a reference to the original method and replace it with your own:
# Save original method
origParseStatement := p.parseStatement.bind(p)
# Replace with custom implementation
p.parseStatement = |s: State|
if s.read.. '@custom'
# Handle custom syntax
return true
# Fall back to original
return origParseStatement(s)
Common patterns:
Use p.pattern(regex, name) to create regex patterns for token matching. It automatically adds the sticky (/y) flag and provides descriptive error messages:
IDENTIFIER := p.pattern.. /[a-zA-Z_$][0-9a-zA-Z_$]*/ "identifier"
NUMBER := p.pattern.. /[0-9]+/ "number"
TAG := p.pattern.. /[a-z][a-z0-9-]*/ "tag-name"
When a token fails to match, error messages will show the descriptive name (e.g., "expected
The State object (s) provides methods for reading input and emitting output:
Reading Input:
s.read(pattern...) - Consume tokens, returns undefined if no matchs.peek(pattern...) - Look ahead without consumings.accept(pattern...) - Read and emit tokenss.must(value) - Throw error if value is falsyEmitting Output:
s.emit(text...) - Add text to outputState Management:
s.snapshot() - Create checkpoint that can be revertedsnapshot.revert() - Revert input and output to checkpointsnapshot.revertOutput() - Revert only outputs.parseGroup(opts, func) - Parse delimited groupsPosition Info:
s.inLine - Current input line numbers.hasMore() - Check if more input remainsA more complex plugin can add entirely new syntax. Here's a simplified markup plugin that transforms :div.class "text" into function calls:
# @file: markup.tab
tabscript 2.0
import type {Parser, State, Options} from "tabscript"
export default function|p: Parser, options: Options|
funcName := "UI"
TAG := p.pattern.. /[a-zA-Z][a-zA-Z0-9-]*/ "tag-name"
# Save original parseStatement
origParseStatement := p.parseStatement.bind(p)
p.parseStatement = |s: State|
if !s.read.. ':'
return origParseStatement(s)
s.emit.. funcName + '(`'
# Parse tag name
s.accept.. TAG
# Parse classes (.class)
while s.read.. '.'
s.emit.. '.'
s.must.. s.accept.. TAG
s.emit.. '`'
# Parse text content
snap := s.snapshot()
s.emit.. ','
if !p.parseExpression(s)
snap.revertOutput()
s.emit.. ');'
return true
Usage:
tabscript 2.0
import plugin "./markup.tab"
:div.container.highlight "Hello world"
# Transpiles to: UI(`div.container.highlight`, "Hello world");
Return false on no match - Your plugin should return false if it doesn't recognize the syntax, allowing other plugins or the default parser to try.
Use snapshots for backtracking - If you start parsing and realize the syntax doesn't match, use snapshot().revert() to undo changes.
Check options.js - If you emit type annotations, check options.js and skip them when outputting JavaScript.
Test thoroughly - Create .tab and .ts test files to verify your plugin output matches expectations.
Bind original methods - When saving a reference to an original method, use .bind(p) to preserve the correct this context.
All the code examples on this page are interactive! Click the "Edit" button on any example to modify the code and see the transpiled output update in real-time. Use the checkbox to toggle between TypeScript and JavaScript output.
tabscript <input.tab> [options]
Options:
--output <file> Output file
--js Transpile to JavaScript
--whitespace <mode> preserve (default) or pretty
--debug Show debug output
--recover Attempt to recover from errors