TJS vs TypeScript vs JavaScript
Every difference on this page is executed, not asserted. Each snippet is run through
tsc --strict and through TJS on every test run, and the table below reports what those
compilers actually did — not what someone believed when they wrote the page.
That matters more than it sounds. One review cycle of this project turned up six
documented behaviours that did not exist: an arrow return syntax that was never
implemented, a predicate => form that parsed and validated nothing, .d.ts stubs that
were never emitted, annotations documented as checked that resolved to any, an editor
completion suggesting a form the compiler rejects, and a playground page teaching nine
abolished directives. Not one was caught by reading.
"rejected" means the compiler refused it. A value means it compiled and that is what it printed.
Assigning a number to a DOM string property
declare const input: HTMLInputElement
input.value = 42
The same program in TJS (the snippet above is TypeScript-only syntax):
class Input {
constructor() { this._v = "" }
get value() { return this._v }
set value(x) { this._v = String(x) }
}
const input = unsafe new Input()
input.value = 42
console.log(input.value)
| TypeScript | TJS | |
|---|---|---|
| result | rejected | 42 |
The DOM spec coerces to a string on assignment. TypeScript models value as a plain string property, so it rejects code the platform is specified to accept.
typeof null
console.log(typeof null)
| TypeScript | TJS | |
|---|---|---|
| result | object |
null |
A 1995 bug JavaScript cannot fix without breaking the web. TypeScript inherits it; TJS reports what the value actually is.
== between a string and a number
console.log('5' == 5)
| TypeScript | TJS | |
|---|---|---|
| result | rejected | false |
TypeScript CATCHES this one — TS2367, "no overlap" — whenever it can see both types statically. TJS makes it false at runtime instead, which also covers the case where TypeScript cannot see them (next row).
== when the type is not statically known
const a = JSON.parse('"5"')
console.log(a == 5)
| TypeScript | TJS | |
|---|---|---|
| result | true |
false |
The honest version of the previous row. Once a value is any — which is what arrives from JSON, the DOM or a network — TypeScript has nothing to compare and the coercion is back. TJS never coerces, because the check happens where the value is.
var
var x = 1
console.log(x)
| TypeScript | TJS | |
|---|---|---|
| result | 1 |
rejected |
Function-scoped hoisting is a hazard with no remaining use. unsafe var x = 1 keeps it at a single site when a port needs it.
new Date()
const d = new Date(0)
console.log(d.getTime())
| TypeScript | TJS | |
|---|---|---|
| result | 0 |
rejected |
Date is mutable and timezone-dependent. Timestamp is epoch milliseconds and pure; unsafe new Date(x) is the per-site escape.
Distinguishing an integer from a float
function f(n: int) { return n }
console.log(String(f(2.5)).slice(0, 22))
| TypeScript | TJS | |
|---|---|---|
| result | — | MonadicError: Expected |
TypeScript has one numeric type, so "this is a count/index/id" is inexpressible and ends up policed by comments. int, unsigned and float name the distinction.
A type that survives to runtime
function greet(name: '') { return 'hi ' + name }
console.log(String(greet(42)).slice(0, 22))
| TypeScript | TJS | |
|---|---|---|
| result | — | MonadicError: Expected |
TypeScript erases annotations before the program runs, so a value arriving from JSON, the DOM or a network is unchecked. TJS checks at the boundary and returns a MonadicError rather than throwing.
The annotation IS a test
function add(a: 2, b: 3): 5 { return a + b }
console.log('ok')
| TypeScript | TJS | |
|---|---|---|
| result | — | ok |
A return example is a worked example, compared by deep equality at build time. add(2, 3) must be 5 — change the body to a - b and the build fails, with no test file and no runner.
A wrong worked example fails the build
function add(a: 2, b: 3): 6 { return a + b }
| TypeScript | TJS | |
|---|---|---|
| result | — | rejected |
The other half of the previous row: the example is checked, not decoration. TypeScript has no equivalent — a return type cannot be wrong about a value.
Property names that declare their own types
Type Prefixed {
example: {}
predicate(o) {
return Object.entries(o).every(([k, v]) =>
k.startsWith('is') ? typeof v === 'boolean' : true
)
}
}
function render(p: Prefixed) { return 1 }
console.log(String(render({ isOpen: 'yes' })).slice(0, 22))
| TypeScript | TJS | |
|---|---|---|
| result | — | MonadicError: Expected |
An index signature forces one type across all keys and a mapped type needs them enumerated in advance, so TypeScript cannot express a convention over an OPEN key set. A predicate reads the name and decides.
Terse predicate: predicate => expr
Type Even {
example: 2
predicate => Even % 2 === 0
}
console.log(Even.check(4), Even.check(3))
| TypeScript | TJS | |
|---|---|---|
| result | — | true false |
Mirrors an arrow function body: => implies the return, and the type name binds to the value under test. It used to parse and accept every value, then was rejected outright rather than ignored; now it is normalised into the function form, so it inherits predicate verification and fuel bounding rather than re-implementing them.
Block predicate: predicate { return expr }
Type Even {
example: 2
predicate { return Even % 2 === 0 }
}
console.log(Even.check(4), Even.check(3))
| TypeScript | TJS | |
|---|---|---|
| result | — | true false |
The multi-line half of the same rule — a { } body requires return, exactly as in JavaScript. Nothing new to learn, which is the argument for this spelling over an implicit last expression.
A parameterized type needs no predicate to check its parameter
Type Box<T> {
example: { value: T }
}
console.log(Box(0).check({ value: 7 }), Box(0).check({ value: 's' }))
| TypeScript | TJS | |
|---|---|---|
| result | — | true false |
The example already says WHERE the parameter goes, so T applied at that slot is derivable and writing predicate(x, T) { return T(x.value) } restates it. Until this was built, omitting the predicate emitted Generic([...], () => true) — a parameterized type that accepted every value while looking like it checked one.
Type arguments in an annotation: b: Box<int>
Type Box<T> {
predicate(x, T) { return T(x.value) }
}
function unbox(b: Box<int>) { return b.value }
console.log(String(unbox({ value: 1.5 })).slice(0, 22))
| TypeScript | TJS | |
|---|---|---|
| result | — | MonadicError: Expected |
A parameterized type applied to arguments is a CALL at run time, and a primitive argument has no runtime binding — so it becomes a PREDICATE, the only representation available for a type that is not a value. The applied type is hoisted to a module-level const named after the annotation, so it is built once rather than per call and the emitter's existing declared-type path handles it unchanged. Composes: Box<Box<int>> works because a parameterized type is itself a valid type argument.
Two functions with the same name
function area(w: number) { return w * w }
function area(w: number, h: number) { return w * h }
console.log(area(3), area(3, 4))
The same program in TJS (the snippet above is TypeScript-only syntax):
function area(w: 0.0) { return w * w }
function area(w: 0.0, h: 0.0) { return w * h }
console.log(area(3.0), area(3.0, 4.0))
| TypeScript | TJS | |
|---|---|---|
| result | rejected | 9 12 |
TypeScript HAS overloads, but they are signature declarations over ONE implementation — TS2393 for a second body — and they are erased entirely, so you write the dispatch by hand. TJS merges same-name declarations into a real arity/type dispatcher, so each case is its own function.
new on a user class
class P { constructor(x: 0) { this.x = x } }
const p = new P(1)
| TypeScript | TJS | |
|---|---|---|
| result | — | rejected |
P(1) and new P(2) produce identical objects — a TJS class is CALLED — so new was decoration with the look of significance. Scoped to classes declared in the file: for a built-in new is MANDATORY (new Float32Array(4) throws without it), a limit found by shipping the general rule and watching eight examples break within a minute.
Calling a class without new
class P { constructor(x: 0) { this.x = x } }
const p = P(1)
console.log(p.x)
| TypeScript | TJS | |
|---|---|---|
| result | — | 1 |
A TJS class is called, not constructed — new adds nothing. In JavaScript (and TypeScript) calling a class without new is a TypeError, so the ceremony is mandatory even though it carries no information.
Adding a row
Edit src/lang/differences.ts, then run bun run docs:differences. If
differences.test.ts disagrees with your row, it is reporting the language as it is —
which is the point. A row that cannot be executed does not belong on this page.