TypeScript
djs runs TypeScript directly. A host turns it on per sandbox (typescript: true in the
JavaScript interface, --ts on the command
line), and the compiler then reads TypeScript syntax in the same single pass in
which it reads JavaScript. There is no type checker and no separate transpile step: the
types are erased while parsing, and the few TypeScript constructs that do something at
run time compile straight to bytecode.
In short. Types are skipped by the parser, so TypeScript costs nothing at run time and almost nothing at compile time. Enums, namespaces and parameter properties run as
tscemits them; decorators follow the standard, with TypeScript’s legacyexperimentalDecoratorsandemitDecoratorMetadatabehind options. Modules are not supported, in TypeScript as in JavaScript.djs strip-typesshows the erasure.
Types are erased while parsing
Everything that only describes types is skipped: annotations, type parameters and
arguments, as, satisfies and the ! assertion, <T>expr casts, interface and
type declarations, declare statements, overload signatures, abstract members,
this parameters and optional markers. Types are not checked: a script whose types are
wrong runs as its JavaScript would.
interface Hit { name: string; stars: number }
type Ranked = Hit & { rank: number };
function rank<T extends Hit>(hits: T[]): (T & { rank: number })[] {
return hits
.toSorted((a, b) => b.stars - a.stars)
.map((h, i) => ({ ...h, rank: i + 1 }));
}
const hits: Hit[] = [{ name: "djs", stars: 120 }, { name: "quickjs", stars: 9000 }];
const top = rank(hits)[0] satisfies Ranked;
const wrong: number = "not checked" as any;
console.log(top.name, top.rank!, wrong);
quickjs 1 not checked
function 0 <anonymous>: params=0 locals=5 stack=5 captures=0
handler [3, 103) -> 103 depth 0
0 async_start loc0 (<state>)
3 fclosure child0
8 put_loc1 (rank)
9 object
10 push_const8 #0 ("djs")
12 define_field "name" ic0
19 push_i8 120
21 define_field "stars" ic1
28 object
29 push_const8 #1 ("quickjs")
31 define_field "name" ic2
38 push_i32 9000
43 define_field "stars" ic3
50 array_from 2
53 put_loc2 (hits)
54 get_loc1 (rank) ; 9:13
55 undefined
56 get_loc2 (hits) ; 9:18
57 call1 ; 9:17
58 push0
59 get_elem ; 9:23
60 put_loc3 (top)
61 push_const8 #2 ("not checked")
63 put_loc8 loc4 (wrong)
65 get_global "console" ic4 ; 11:1
72 get_method "log" ic5 ; 11:8
79 get_loc3 (top) ; 11:13
80 get_field "name" ic6 ; 11:16
87 get_loc3 (top) ; 11:23
88 get_field "rank" ic7 ; 11:26
95 get_loc8 loc4 (wrong) ; 11:34
97 call3 ; 11:12
98 drop
99 undefined
100 async_return loc0 (<state>)
103 async_reject loc0 (<state>)
function 0.0 rank: params=1 locals=0 stack=3 captures=0
0 get_arg0 ; 4:10
1 get_method "toSorted" ic0 ; 5:5
8 fclosure child0
13 call1 ; 5:14
14 get_method "map" ic1 ; 6:5
21 fclosure child1
26 call1 ; 4:3
27 return
function 0.0.0 <anonymous>: params=2 locals=0 stack=2 captures=0
0 get_arg1 ; 5:25
1 get_field "stars" ic0 ; 5:26
8 get_arg0 ; 5:35
9 get_field "stars" ic1 ; 5:36
16 sub ; 5:33
17 return
function 0.0.1 <anonymous>: params=2 locals=0 stack=3 captures=0
0 object
1 get_arg0 ; 6:26
2 copy_data_props 0
4 get_arg1 ; 6:35
5 push1
6 add ; 6:37
7 define_field "rank" ic0
14 return
Run the example to walk the VM's arena.
Run the example to see it suspend.
The rules for telling a type from an expression (a generic call f<T>(x) from two
comparisons, an arrow function with a return type from a parenthesized expression, where
a line break ends a contextual keyword) are TypeScript’s, and so are the syntax errors,
with QuickJS’s wording. Semantics follow tsc with isolatedModules and
useDefineForClassFields: class fields are defined, not assigned.
Trade-off. Erasing in the parser over transpiling first. Because djs compiles without a syntax tree, it can skip types as it meets them, and a TypeScript script compiles about as fast as the same script without its types. The cost: no type information reaches the compiler, so there is no type checking and nothing that needs types (
const enumacross files,emitDecoratorMetadatafor types it cannot see) works beyond what one file says.
What runs
A few TypeScript constructs have run-time meaning, and djs compiles them as tsc emits
them:
- Enums become objects, numeric members with a reverse mapping. A
const enum’s members are folded into the code that uses them; its object exists too. - Namespaces become objects filled by a function, merged across declarations; a namespace with only types is erased.
- Parameter properties (
constructor(public x: number)) define and assign the field, intsc’s order relative to the other fields.
enum Level { Debug, Info, Warn = "warn" }
const enum Limit { Max = 3 }
namespace Log {
export const lines: string[] = [];
export function add(level: Level, text: string) {
if (lines.length < Limit.Max) lines.push(`${Level[level] ?? level}: ${text}`);
}
}
class Job {
constructor(public readonly id: number, private attempts = 0) {}
retry() { return ++this.attempts; }
}
Log.add(Level.Info, "started");
Log.add(Level.Warn, "slow");
const job = new Job(7);
job.retry();
console.log(Log.lines.join("; "), JSON.stringify(job));
Info: started; warn: slow {"id":7,"attempts":1}
function 0 <anonymous>: params=0 locals=8 stack=6 captures=0
handler [11, 217) -> 217 depth 0
0 undefined
1 new_box_init loc1 (Level)
4 undefined
5 new_box_init loc2 (Limit)
8 async_start loc0 (<state>)
11 fclosure child0
16 undefined
17 get_box loc1 (Level) ; 1:1
20 dup
21 jump_if_true8 -> 29
23 drop
24 object
25 dup
26 put_box loc1 (Level)
29 call1
30 drop
31 fclosure child1
36 undefined
37 get_box loc2 (Limit) ; 2:7
40 dup
41 jump_if_true8 -> 49
43 drop
44 object
45 dup
46 put_box loc2 (Limit)
49 call1
50 drop
51 fclosure child2
56 undefined
57 get_loc3 (Log) ; 3:11
58 dup
59 jump_if_true8 -> 65
61 drop
62 object
63 dup
64 put_loc3 (Log)
65 call1
66 drop
67 undefined
68 undefined
69 new_box loc6 (<fields 0>)
72 fclosure child3
77 define_class 0
79 fclosure child4
84 define_class_method "retry" 0
90 undefined
91 put_box loc6 (<fields 0>)
94 drop
95 dup
96 put_loc8 loc5 (Job)
98 put_loc8 loc4 (Job) ; 9:1
100 get_loc3 (Log) ; 13:1
101 get_method "add" ic0 ; 13:4
108 get_box loc1 (Level) ; 13:9
111 get_field "Info" ic1 ; 13:14
118 push_const8 #0 ("started")
120 call2 ; 13:8
121 drop
122 get_loc3 (Log) ; 14:1
123 get_method "add" ic2 ; 14:4
130 get_box loc1 (Level) ; 14:9
133 get_field "Warn" ic3 ; 14:14
140 push_const8 #1 ("slow")
142 call2 ; 14:8
143 drop
144 get_loc8 loc4 (Job) ; 15:17
146 push_i8 7
148 construct 1 ; 15:20
151 set_loc8 loc7 (job) ; 16:1
153 get_method "retry" ic4 ; 16:4
160 call0 ; 16:10
161 drop
162 get_global "console" ic5 ; 17:1
169 get_method "log" ic6 ; 17:8
176 get_loc3 (Log) ; 17:13
177 get_field "lines" ic7 ; 17:16
184 get_method "join" ic8 ; 17:22
191 push_const8 #2 ("; ")
193 call1 ; 17:27
194 get_global "JSON" ic9 ; 17:35
201 get_method "stringify" ic10 ; 17:39
208 get_loc8 loc7 (job) ; 17:50
210 call1 ; 17:49
211 call2 ; 17:12
212 drop
213 undefined
214 async_return loc0 (<state>)
217 async_reject loc0 (<state>)
function 0.0 <anonymous>: params=1 locals=0 stack=4 captures=0
0 get_arg0
1 get_arg0
2 push0
3 insert2
4 put_field "Debug" ic0 ; 1:14
11 push_const8 #2 ("Debug")
13 put_elem
14 get_arg0
15 get_arg0
16 push1
17 insert2
18 put_field "Info" ic1 ; 1:21
25 push_const8 #3 ("Info")
27 put_elem
28 get_arg0
29 push_const8 #4 ("warn")
31 put_field "Warn" ic2 ; 1:27
38 return_undef
function 0.1 <anonymous>: params=1 locals=0 stack=4 captures=0
0 get_arg0
1 get_arg0
2 push3
3 insert2
4 put_field "Max" ic0 ; 2:20
11 push_const8 #1 ("Max")
13 put_elem
14 return_undef
function 0.2 <anonymous>: params=1 locals=2 stack=2 captures=2
0 get_arg0
1 new_box_init loc0 (<ns 2>)
4 fclosure child0
9 put_loc1 (add)
10 array_from 0
13 get_box loc0 (<ns 2>) ; 4:16
16 swap
17 put_field "lines" ic0
24 get_box loc0 (<ns 2>)
27 get_loc1 (add)
28 put_field "add" ic1 ; 5:19
35 return_undef
function 0.2.0 add: params=2 locals=0 stack=5 captures=3
0 get_cap cap2 (<ns 2>) ; 6:9
3 get_field "lines" ic0
10 get_field "length" ic1 ; 6:14
17 push3 ; 6:24
18 lt ; 6:22
19 jump_if_false8 -> 59
21 get_cap cap2 (<ns 2>) ; 6:35
24 get_field "lines" ic2
31 get_method "push" ic3 ; 6:40
38 get_cap cap1 (Level) ; 6:49
41 get_arg0 ; 6:55
42 get_elem ; 6:54
43 dup
44 is_nullish
45 jump_if_false8 -> 49
47 drop
48 get_arg0 ; 6:65
49 to_string ; 6:47
50 push_const8 #0 (": ")
52 get_arg1 ; 6:75
53 to_string ; 6:73
54 concat 3
57 call1 ; 6:45
58 drop
59 return_undef
function 0.3 Job: params=2 locals=3 stack=4 captures=1
0 special_object 4
2 put_loc0 (this)
3 check_ctor ; 10:14
4 get_loc0 (this)
5 dup
6 push_atom "id"
11 undefined
12 define_class_field
13 drop
14 dup
15 push_atom "attempts"
20 undefined
21 define_class_field
22 drop
23 get_cap cap0 (<fields 0>)
26 dup
27 is_undefined
28 jump_if_true8 -> 33
30 pick 1
32 call0
33 drop
34 drop
35 get_arg0
36 put_loc1 (id)
37 get_arg1
38 dup
39 is_undefined
40 jump_if_false8 -> 44
42 drop
43 push0
44 put_loc2 (attempts)
45 get_loc0 (this) ; 10:64
46 dup
47 get_loc1 (id)
48 put_field "id" ic0
55 dup
56 get_loc2 (attempts)
57 put_field "attempts" ic1
64 drop
65 return_undef
function 0.4 retry: params=0 locals=1 stack=3 captures=0
0 special_object 4
2 put_loc0 (this)
3 get_loc0 (this) ; 11:22
4 dup ; 11:26
5 get_field "attempts" ic0
12 inc ; 11:20
13 insert2
14 put_field "attempts" ic1 ; 11:26
21 return
Run the example to walk the VM's arena.
Run the example to see it suspend.
Decorators
Decorators follow the standard (the TC39 proposal TypeScript 5 implements): class,
method, accessor, field and accessor decorators, with their context objects,
addInitializer and Symbol.metadata. They work in JavaScript too.
function logged(fn: Function, ctx: ClassMethodDecoratorContext) {
return function (this: unknown, ...args: unknown[]) {
console.log(`${String(ctx.name)}(${args.join(", ")})`);
return fn.apply(this, args);
};
}
class Calc {
@logged add(a: number, b: number) { return a + b; }
}
console.log(new Calc().add(40, 2));
add(40, 2) 42
function 0 <anonymous>: params=0 locals=7 stack=8 captures=0
handler [3, 162) -> 162 depth 0
0 async_start loc0 (<state>)
3 fclosure child0
8 put_loc1 (logged)
9 undefined
10 undefined
11 new_box loc4 (<fields 0>)
14 new_box loc6 (<dec>1)
17 fclosure child1
22 define_class 0
24 get_loc1 (logged) ; 8:4
25 undefined
26 array_from 2
29 put_loc8 loc5 (<dec>0)
31 fclosure child2
36 define_class_method "add" 0
42 pick 1
44 push_false
45 call_intrinsic %DecoratorStart 2 ; 7:1
50 put_box loc6 (<dec>1)
53 dup
54 push_atom "add"
59 undefined
60 get_loc8 loc5 (<dec>0)
62 get_box loc6 (<dec>1)
65 push1
66 call_intrinsic %ApplyDecorators 6
71 drop
72 fclosure child3
77 set_home_object
78 put_box loc4 (<fields 0>)
81 drop
82 dup
83 push_atom "Calc"
88 undefined
89 undefined
90 get_box loc6 (<dec>1)
93 push0
94 call_intrinsic %ApplyDecorators 6
99 dup
100 put_loc3 (Calc)
101 get_box loc6 (<dec>1)
104 push1
105 pick 2
107 call_intrinsic %DecoratorRunExtras 3
112 drop
113 nip
114 get_box loc6 (<dec>1)
117 push3
118 pick 2
120 call_intrinsic %DecoratorRunExtras 3
125 drop
126 put_loc2 (Calc)
127 get_global "console" ic0 ; 10:1
134 get_method "log" ic1 ; 10:8
141 get_loc2 (Calc) ; 10:17
142 construct 0 ; 10:21
145 get_method "add" ic2 ; 10:23
152 push_i8 40
154 push2
155 call2 ; 10:27
156 call1 ; 10:12
157 drop
158 undefined
159 async_return loc0 (<state>)
162 async_reject loc0 (<state>)
function 0.0 logged: params=2 locals=2 stack=1 captures=0
0 get_arg0
1 new_box_init loc0 (fn)
4 get_arg1
5 new_box_init loc1 (ctx)
8 fclosure child0
13 return
function 0.0.0 <anonymous>: params=0 locals=2 stack=7 captures=2
0 special_object 4
2 put_loc1 (this)
3 rest arg0
6 put_loc0 (args)
7 get_global "console" ic0 ; 3:5
14 get_method "log" ic1 ; 3:12
21 get_global "String" ic2 ; 3:20
28 undefined
29 get_cap cap0 (ctx) ; 3:27
32 get_field "name" ic3 ; 3:30
39 call1 ; 3:26
40 to_string ; 3:18
41 push_const8 #0 ("(")
43 get_loc0 (args) ; 3:40
44 get_method "join" ic4 ; 3:44
51 push_const8 #1 (", ")
53 call1 ; 3:49
54 to_string ; 3:38
55 push_const8 #2 (")")
57 concat 4
60 call1 ; 3:16
61 drop
62 get_cap cap1 (fn) ; 4:12
65 get_method "apply" ic5 ; 4:14
72 get_loc1 (this) ; 4:21
73 get_loc0 (args) ; 4:27
74 call2 ; 4:5
75 return
function 0.1 Calc: params=0 locals=1 stack=3 captures=1
0 special_object 4
2 put_loc0 (this)
3 check_ctor ; 10:1
4 get_loc0 (this)
5 get_cap cap0 (<fields 0>)
8 dup
9 is_undefined
10 jump_if_true8 -> 15
12 pick 1
14 call0
15 drop
16 drop
17 return_undef
function 0.2 add: params=2 locals=0 stack=2 captures=0
0 get_arg0 ; 8:46
1 get_arg1 ; 8:50
2 add ; 8:48
3 return
function 0.3 <anonymous>: params=0 locals=1 stack=3 captures=1
0 special_object 4
2 put_loc0 (this)
3 jump8 -> 6
5 return_undef
6 get_cap cap0 (<dec>1)
9 push2
10 get_loc0 (this) ; 9:2
11 call_intrinsic %DecoratorRunExtras 3
16 drop
17 return_undef
Run the example to walk the VM's arena.
Run the example to see it suspend.
Legacy decorators. Code written for TypeScript’s experimentalDecorators expects the
older semantics: decorators get the prototype, the key and a property descriptor, and
parameters can be decorated. The experimentalDecorators option (--experimental-decorators)
selects them, and emitDecoratorMetadata (--emit-decorator-metadata, which implies
the first) also calls Reflect.metadata with design:type, design:paramtypes and
design:returntype, as far as the annotations in the file say. As in TypeScript, the
script (or a library it includes) provides Reflect.metadata.
function log(target: object, key: string, desc: PropertyDescriptor) {
console.log("decorating", key);
}
function param(target: object, key: string, index: number) {
console.log("parameter", index, "of", key);
}
class Greeter {
@log greet(@param name: string) { return "hi " + name; }
}
// parameter 0 of greet
// decorating greet
What is not supported
- Modules in scripts. In a script
importandexportare syntax errors, alsoimport typeandexport type: a script is one piece of code, and its host gives it its functions as globals or parameters (bindings). A TypeScript module, which native hosts can load, has them, and its type-only imports and exports are erased. - JSX and
.tsx.<T>expris always a cast. - Type checking, and everything that needs types from another file.
- In the slim build, decorators.
djs strip-types
djs strip-types file.ts prints the JavaScript the compiler sees: the file with every
erased range replaced by spaces, line breaks kept, so positions in error messages and
stack traces are the same in both. The output is byte for byte what the ts-blank-space
tool produces for the same input, which the tests check against its output for
TypeScript’s own sources. It accepts module syntax, which the compiler itself does not,
so it can be compared on any TypeScript file. Enums, namespaces and parameter
properties are kept as written: they are not erasable.
$ cat a.ts
class A { x = 1 } const a: A = new A(); console.log(a.x)
$ djs strip-types a.ts
class A { x = 1 } const a = new A(); console.log(a.x)
Where to look
crates/djs-core/src/compiler/: the parser and its type skipper.builtins/decorators.ts: the decorator runtime.tests/strip-types/: the strip-types cases with ts-blank-space’s output.