Get more of your program typed
What the typed percentage counts, how to read bun compile --explain, and how to fix each cause
bun compile ends with the percentage of your program's property accesses that are typed.
Untyped code works, but runs slower. Run with --explain to see what to fix.
75% of property accesses are one load: 6 free, 1 tested, 1 by name (8)A typed access is one load or one store at a fixed offset. An untyped access runs a test of its own first, or looks the property up by name at run time. The higher the number, the faster the executable. How much faster depends on the program.
bun compile --explain lists what's lowering the number, worst first. Each entry has the line where the missing type begins, a fix, and what hangs on that line: property accesses, indirect calls and untyped operators, with an estimate of how much of the run time they take.
What is counted#
A property access is a read (item.price), a write (item.price = 2), an in test or a delete. Each one compiles to one of three things.
| The line says | The compiled code | Counts as |
|---|---|---|
free | One load or store at a fixed offset. The shape is known from the types or from an earlier check | Typed |
tested | A test of its own, then a load or store at a fixed offset. An object that fails the test takes the slow path | Untyped |
by name | A lookup by name | Untyped |
Every class has a fixed layout: its fields sit at fixed offsets, like a struct in Go or Rust. So does every object type that an object literal is created as. An object's shape says which fixed layout it has.
An access is free when the compiler knows every shape the object can have at that point. It finds out in two ways.
- From your types. Typed code trusts its declared types. Where the build sees every caller and each one passes the declared types, nothing is checked at run time. Otherwise a guard at the function's entry checks the arguments, and Bun also compiles a slow version of that function, which works like plain JavaScript on the value that didn't match. Behind the guard, a parameter declared
item: Itemis anItem. - By tracking objects. The compiler follows each object from the
newor the object literal that creates it to every use: through variables, parameters, fields, arrays, maps and calls, in TypeScript and in JavaScript files.
Two things break the proof.
- An object the compiler didn't see being created can reach the access. It comes from
JSON.parse, from a native function, or from a module loaded at run time. - Something can change the objects after they're created. A
delete,Object.setPrototypeOf, a store with a computed key, or a call into code that the compiler can't see.
An any or a .js file isn't a cause by itself. If only objects that the compiler knows reach an any, a read from it is proven.
The count is taken after optimization, so it describes the machine code, not your source.
- An access that the optimizer removes isn't counted. That happens to an object that never escapes the function that creates it.
- An access is counted once, however many copies the compiler makes of it by inlining a function or by compiling it twice. The copy in the function's own fast version decides how it counts.
- Only hot code gets guards. In cold code, such as the top level of a module outside of a loop, a guard isn't worth the code size, and the access is a lookup by name.
So the total moves when you change code nearby. Watch the percentage, not the total.
| Not counted | Why |
|---|---|
stock.add(item) | A method call is a call. The report counts calls separately |
{ sku: "A-1", price: 2.5 } | An object literal fills its fields as it creates the object |
items.length, items[0], items.push(item), map.get(key), text.slice(1) | Built-in operations, where Bun knows that items is an array, map a Map and text a string |
console.log(item), Math.max(a, b), Bun.file(path) | Calls of built-in functions |
Anything in Bun's own modules, such as node:fs | It isn't your code |
A read with a computed key, such as prices[name] on a Record<string, number>, is counted as by name.
What else the report counts#
The percentage is about property accesses. --explain also counts two other kinds of operations that are slow for the same reason, and lists them under the same causes.
| The entry says | Counts |
|---|---|
indirect calls | Calls and new where the compiler doesn't know the callee, so it finds the function at run time and can't inline it |
untyped operators | if (x), !x, a && b, ===, ==, <, +, - and the other operators, typeof, instanceof, for...of and { ...x } on a value whose type the compiler doesn't know |
With known types each of these is a few instructions. Without, it's a call to a routine that looks at the value first. An operator where the compiler knows one side, or knows that the value is a string or a number, isn't counted.
An access counts for the file it's in, wherever Bun inlines it. Code in node_modules is counted on its own Dependencies: line, above yours.
Dependencies: 3.1% of property accesses are one load: 210 free, 1,320 tested, 5,244 by name (6,774)
92.4% of property accesses are one load: 1,204 free, 61 tested, 38 by name (1,303)Bun rounds down, so 100% means every access. --explain then has nothing to report about your own code. Bun prints the hint about --explain when the number for your own code is under 95%.
Read an entry#
One object in this list comes from JSON.parse, so item.price in the loop needs a guard.
interface Item {
sku: string;
price: number;
}
const items: Item[] = [{ sku: "A-1", price: 2.5 }];
const saved: any = JSON.parse('{ "sku": "B-2", "price": 10 }');
items.push(saved);
let total = 0;
for (const item of items) total += item.price;
console.log(total);bun compile ./main.ts --explainnote: Untyped code works, but runs slower. 1 cause:
7 | const saved: any = JSON.parse('{ "sku": "B-2", "price": 10 }');
^
note: 1. unknown-object: An object from untyped code is used as this type here, so it can have any shape.
at main.ts:7:25
note: 1 of 1 untyped access
note: 1 untyped operator
note: about 100% of the time in untyped operations, by how often the compiler expects each to run
note: types: 'Item'
note: why (external-data): The value comes from outside the program (JSON.parse, response.json(), structuredClone, a message).
note: fix: build a new object of this type from it where it enters typed code.
note: https://bun.com/docs/compiler/explain#unknown-objectThe program has one property access, item.price, and one operator that works on what it reads, +=. An Item in the list can be the object literal from line 6 or whatever JSON.parse returned, so the compiled code checks the shape of each item before it reads from it.
| Line | Says |
|---|---|
| The first line | The rank, the code of the cause, and what happens at the location |
at | Where the missing type begins, with the source line above it. A cause with no single location has neither |
1 of 1 untyped .. | How many accesses the fix can speed up, out of all the untyped accesses |
3 indirect calls .. | How many indirect calls and untyped operators hang on the same line. Only if there are any |
about 100% .. | The estimate that the entries are sorted by |
12 of these stay .. | How many of them have another cause as well. Only if there are any |
in function | The function around the location. Code at the top level of a module, as here, has no such line |
types: | Up to eight of the types that the cause affects, and a count of the rest. An object literal with no declared type has no name to list |
limit | The compiler limit that was hit, its value and what it limits. Only limit-reached has it |
why | Why the access isn't proven, with a code of its own. Not every entry has one |
fix: | The fix |
| The link | The section of this page about the cause |
An operation is listed under exactly one entry, so the entries add up to the number of untyped accesses.
| The count reads | The accesses are |
|---|---|
2 of 5 untyped accesses | In your own code |
4 of 4 untyped accesses in dependencies | In node_modules |
2 of 5 untyped accesses, and 1,200 of 4,000 in dependencies | In both |
It prints the first 20 entries and counts the rest. --explain=json prints all of them.
The location is where it begins#
A value of unknown type spreads. What you read from it has no type, what it returns when you call it has no type, and a function that you hand to it can be called with anything. Bun follows all of that back, through variables, parameters, fields and calls, in all files, to the one operation that no earlier operation explains. That's the location of the entry.
export const settings: any = JSON.parse(text); // the entry points hereimport { settings } from "./settings";
function width(panel: any) {
return panel.size.width * 2; // two accesses and an operator that count for settings.ts:1
}
width(settings.panel); // one moreFor a function or an object that code outside of the program gets hold of, the location is where you hand it over, not where you use it.
A value can have more than one such beginning: two calls of JSON.parse whose results reach the same function, for example. Bun lists the operation under the nearest one and prefers a line of your code or your dependencies to a line in Bun's own modules. The entry says how many of its operations stay untyped until other entries are fixed too.
How entries are ranked#
Bun sorts the entries by an estimate of the time that the fix saves in your own code, then in dependencies.
- Each operation has a cost: about how many instructions it executes beyond what it would with types. A lookup by name costs 28, a test 2, an indirect call 13, an untyped operator 6 to 60.
- The cost is multiplied by how often the compiler expects the operation to run. This is the estimate that it uses to decide what to inline and where guards are worth their size: code in a loop counts about ten times the code around it, and the functions that are called the most count up to a hundred times the ones that are called the least. It comes from the structure of your program. Nothing is measured.
So five accesses in an inner loop rank above a hundred that run once at startup. The plain counts are always printed next to the estimate.
Entries that you can't fix come last, whatever they cost: missed-optimization, unknown, analysis-disabled and entries whose location is in one of Bun's own modules.
Every cause belongs to a family. Pass a family to --explain to see only its causes, one per line.
bun compile ./main.ts --explain=unknown-codeaot: unknown-code: unknown-object at main.ts:7:25, 1 access, 1 untyped operator, types 'Item', why external-dataHere is the fix. readItem narrows the parsed value and builds a new Item from it.
interface Item {
sku: string;
price: number;
}
function readItem(value: unknown): Item {
if (typeof value !== "object" || value === null) throw new Error("item: not an object");
const { sku, price } = value as Record<string, unknown>;
if (typeof sku !== "string") throw new Error("item: sku must be a string");
if (typeof price !== "number") throw new Error("item: price must be a number");
return { sku, price };
}
const items: Item[] = [{ sku: "A-1", price: 2.5 }];
const saved = readItem(JSON.parse('{ "sku": "B-2", "price": 10 }'));
items.push(saved);
let total = 0;
for (const item of items) total += item.price;
console.log(total);bun compile ./main.ts33.3% of property accesses are one load: 1 free, 0 tested, 2 by name (3)Every Item in the list is now an object literal that the compiler sees, so item.price in the loop is free. The two accesses that are left are in readItem. It reads sku and price from the parsed object by name, once for each object that comes in.
Fix the first entry, then run the command again. A fix can uncover another cause behind it: if two things keep Item untyped, the report shows one of them at a time.
bun check --aot --explain prints the same report and writes no executable.
After the entries#
The report ends with the numbers behind the percentage. These are from a larger program.
note: All property accesses, including dependencies:
1,414 free: one load or store at a fixed offset
1,381 tested: a test of its own, then a load or store at a fixed offset
5,282 by name: looked up by name
Calls: 4,870 direct, 2,316 indirect
Files with the most untyped accesses:
src/routes/orders.ts 71.4% typed (85 of 119)
src/config.ts 0% typed (0 of 21)
src/db/rows.ts 88.1% typed (134 of 152)
src/render.ts 96% typed (240 of 250)
src/session.ts 90% typed (72 of 80)
and 3 more
Compiler limits hit:
MaxLayoutsInType = 1,024, hit 2 times (limits object shapes in one inferred type)- The first three numbers are the three ways to compile an access, for your code and your dependencies together.
Callscounts calls of functions and methods, except the built-in functions that Bun resolves at build time. A call is direct if the compiler knows the callee. An indirect call finds it at run time.Files with the most untyped accesseslists up to five of your own files.Compiler limits hitlists the limits this build ran into, and how often. Past a limit the compiler assumes less, so some accesses may be untyped because of it. See limit-reached.
JSON#
--explain=json prints every entry to stdout, one JSON object per line. Here is the entry from the example above, before the fix, split over several lines so you can read it.
bun check --aot ./main.ts --explain=json{
"rank": 1,
"reason": "unknown-object",
"limit": null,
"file": "main.ts",
"line": 7,
"column": 25,
"inDependency": false,
"source": "const saved: any = JSON.parse('{ \"sku\": \"B-2\", \"price\": 10 }');",
"message": "An object from untyped code is used as this type here, so it can have any shape.",
"fix": "build a new object of this type from it where it enters typed code.",
"url": "https://bun.com/docs/compiler/explain#unknown-object",
"accesses": 1,
"dependencyAccesses": 0,
"calls": 0,
"operators": 1,
"fixedByThisAlone": 2,
"estimatedPercent": 100.0,
"function": "",
"types": ["Item"],
"family": "unknown-code",
"why": {
"reason": "external-data",
"message": "The value comes from outside the program (JSON.parse, response.json(), structuredClone, a message).",
"fix": "check its fields, and build a new object of this type from them.",
"url": "https://bun.com/docs/compiler/explain#external-data"
}
}| Field | Contains |
|---|---|
rank | The position in the report, from 1 |
reason, url | The code of the cause, and the section of this page about it |
limit | The compiler limit that was hit, or null |
file, line, column | The location. file is empty and the numbers are 0 if the cause has no single location |
inDependency | true if nothing in your own code hangs on the entry: it only counts for dependencies |
source | The source line, or an empty string |
message, fix | What happens there, and how to fix it |
accesses, dependencyAccesses | How many accesses the fix can speed up, in your own code and in dependencies |
calls, operators | How many indirect calls and untyped operators it can speed up, dependencies included |
fixedByThisAlone | How many of all these have no other cause |
estimatedPercent | The estimate: the entry's share of the time in untyped operations |
function | The function around the location, or an empty string |
types | The names of all the types that the cause affects |
family | The group of causes that the code belongs to: unknown-code, mutation, unproven-type or limit |
why | Why the access isn't proven: a code, a message, a fix and a link of its own. Can be null |
The first line has the numbers, for a script or for CI. This one is from the larger program.
{
"accesses": { "byName": 38, "tested": 61, "free": 1204, "testedSlotOnly": 12, "provenWithoutAnyTest": 1150 },
"dependencyAccesses": {
"byName": 5244,
"tested": 1320,
"free": 210,
"testedSlotOnly": 240,
"provenWithoutAnyTest": 180
},
"calls": { "unknown": 143, "guarded": 0, "proven": 2210 },
"causes": 12
}| Field | Contains |
|---|---|
accesses | The three numbers of the last line of the build, for your own code. testedSlotOnly is the part of tested where the shape is known and only the field's value is tested. provenWithoutAnyTest is the part of free that needs no check anywhere |
dependencyAccesses | The same for the Dependencies: line |
calls | The calls in your own code, counted the same three ways |
causes | The number of entries that follow |
With a coding agent#
Give your agent the command and the goal:
Run `bun compile --explain=json`. Fix the first entry, at its file, line and column, the way its `fix` says, without changing
what the program does. Run the tests, then run the command again. Stop when the first entry's estimatedPercent is small.Read what it changes. A fix that builds a new object makes a copy, so a later change to the copy doesn't reach the original object. If it narrows the value first, it throws on data that doesn't match, where nothing threw before.
Causes#
In the messages, untyped code is code that the compiler can't see: a native function that Bun has no description of, a module loaded at run time, or code from a string. A JavaScript file that's compiled with your program is not untyped code in this sense.
| Family | What the causes have in common | Codes |
|---|---|---|
unknown-code | Your objects meet code or data that the compiler can't see | unknown-object, untyped-base, class-not-top-level, passed-to-builtin, passed-to-untyped-code, written-by-untyped-code, unknown-callee, builtin-result, runtime-code, external-data, caught-value, called-by-unknown-code, read-from-shared-object, unknown-callee-result |
mutation | Something changes an object after it's created | field-deleted, computed-key-delete, property-redefined, property-added, computed-key-store, wrong-type-stored, frozen, prototype-changed, method-shadowed, other-new-target |
unproven-type | The value can't have a fixed layout, or the proof is missing | unknown, analysis-disabled, not-a-plain-object, builtin-base, untyped-collection, missed-optimization, unchecked-value, no-fixed-layout, lost-track, unexpected-shape |
limit | The compiler gave up | limit-reached |
unknown#
The compiler recorded no reason for this slow path.
This is a bug in Bun. Please report it and include the entry.
analysis-disabled#
The analysis that proves this was turned off by a developer option.
--aot-disable, or compile.aotDisable in Bun.build, is an option for working on the compiler. Build without it.
limit-reached#
The analysis gave up here because it hit a compiler limit.
The entry's limit line has the limit, its value and what it limits.
note: limit MaxLayoutsInType = 1,024 (limits object shapes in one inferred type)Split the function or the type to get under it. Compiler limits hit, at the end of the report, lists every limit this build hit.
unknown-object#
An object from untyped code is used as this type here, so it can have any shape.
Every access the object can reach needs a shape check first. The entry points to where the object comes from.
const config: Config = JSON.parse(text);
const config = readConfig(JSON.parse(text)); Take the value as unknown, narrow it, and build the object yourself. The compiler then sees every Config being created. The reads from the untyped value are lookups by name, but they happen once, at the edge of your program.
function readConfig(value: unknown): Config {
if (typeof value !== "object" || value === null) throw new Error("config: not an object");
const { host, port } = value as Record<string, unknown>;
if (typeof host !== "string") throw new Error("config: host must be a string");
if (typeof port !== "number") throw new Error("config: port must be a number");
return { host, port };
}The examples below call such a function readOrder, readItem or readOptions.
If the shape check is cheap enough for you, make sure that it passes: declare the type where you parse. If the text matches, JSON.parse(text) as Config creates the object with the fixed layout of Config.
not-a-plain-object#
An array, a function or a built-in object is used as this type here.
TypeScript allows it where the properties match, but such an object keeps its own shape.
interface Named {
name: string;
}
function nameOf(value: Named): string {
return value.name;
}
nameOf(user);
nameOf(nameOf);
nameOf({ name: nameOf.name }); Pass an object literal or a class instance, or declare the parameter with the type that you actually pass.
builtin-base#
This class extends a built-in class. The built-in constructor allocates the instances, so their fields can't be at fixed offsets.
This is fine for error classes. For a class with many instances, hold the built-in object in a field instead of extending it.
class Rows extends Array<Row> {
class Rows {
list: Row[] = [];
}untyped-base#
This class extends a class that comes from untyped code.
The base class decides how the object is created, so every access to an instance needs a shape check.
const { Base } = await import(process.argv[2]);
class Job extends Base {
class Job {
base = new Base();
retries = 0;
}Extend one of your own classes, or store the other object in a field.
class-not-top-level#
This class declaration, or one it extends, can be evaluated more than once.
Each evaluation creates a new class with its own prototype.
class Token {
start = 0;
}
function tokenize(text: string) {
class Token {
start = 0;
}
return [new Token()];
}Define the class at the top level of a module, not in a function or a loop.
passed-to-builtin#
An object of this type is passed to a built-in function here that may add, delete or redefine its properties.
Object.assign(box, overrides);
box.width = overrides.width;
box.height = overrides.height; Pass only the fields the function needs, or a copy.
passed-to-untyped-code#
An object of this type is passed to untyped code here, which may add, delete or redefine its properties.
One such object is enough. Every access to the type, anywhere in the program, then needs a shape check.
const plugin = await import(process.argv[2]);
for (const box of boxes) plugin.publish(box);
for (const box of boxes) plugin.publish(box.width, box.height); Pass only the fields the other code needs. Bun reports two built-in functions under this code too.
| You write | Instead |
|---|---|
Object.defineProperty(box, "area", { .. }) | Declare the field, getter or setter in the class |
Object.create(box) | Use a class and extends |
written-by-untyped-code#
Untyped code can write to a field of this type, so the field may hold any value.
const plugin = await import(process.argv[2]);
plugin.reset(counter);
counter.count = plugin.initialCount(); Keep the object out of that code, or pass only the fields it needs.
unknown-callee#
The callee isn't known at build time, so the result has no type.
const handlers = await import(process.argv[2]);
const order = handlers[name](request);
const order = readOrder(handlers[name](request));
console.log(order.total);Call the function directly, or narrow the result.
untyped-collection#
The array, Map or Set this value is read from may contain values of other types.
const items: Item[] = [];
items.push(JSON.parse(line));
items.push(readItem(JSON.parse(line)));
for (const item of items) total += item.price;Only add to the collection from typed code, or narrow the element after reading it.
field-deleted#
A property that the type requires is deleted here.
class Counter {
count: number | undefined = 0;
}
delete (counter as any).count;
counter.count = undefined; Assign undefined, or declare the property optional. A delete of an optional property costs nothing.
computed-key-delete#
A property is deleted here with a computed key.
The key can be any field.
delete (counter as any)[key];
extras.delete(key); Use a Map, or delete a constant key.
property-redefined#
A property of an object of this type is redefined here, for example as a getter or a setter.
class Box {
width = 0;
height = 0;
get area() {
return this.width * this.height;
}
}
Object.defineProperty(box, "area", { get: () => box.width * box.height }); Declare it as a field, a getter or a setter in the class or the object literal.
property-added#
A property that the type does not declare is added to an object of this type here.
class Counter {
count = 0;
label?: string;
}
(counter as any).label = "visits";
counter.label = "visits"; Declare the property in the class or the object type.
computed-key-store#
A property is assigned here with a computed key.
The key can be any field, and the value can have any type.
(counter as any)[key] = 1;
extras.set(key, 1); Use a Map or a Record type for data with computed keys.
wrong-type-stored#
A value of a different type is stored in this field here.
class Counter {
count = 0;
count: number | string = 0;
}
(counter as any).count = "none";Store a value of the declared type, or widen the type of the field.
frozen#
An object of this type is passed to Object.freeze, Object.seal or Object.preventExtensions here.
const defaults: Options = { host: "localhost", port: 3000 };
Object.freeze(defaults);
const defaults: Options = Object.freeze({ host: "localhost", port: 3000 }); Freeze the object where it's created, or freeze a copy.
prototype-changed#
The prototype of an object of this type is changed here.
const counter = new Counter();
Object.setPrototypeOf(counter, null); Create the object with the prototype it needs.
method-shadowed#
A property added to an instance can shadow a method of its class.
class Counter {
count = 0;
reset() {
this.count = 0;
}
reset = () => {
this.count = 0;
};
}
counter.reset = () => {};Declare the property in the class.
other-new-target#
This class is constructed with a different new.target (Reflect.construct, or a subclass from untyped code).
The object then gets the other class's prototype.
const counter = Reflect.construct(Counter, [], Other);
const counter = new Counter(); Construct it with new.
missed-optimization#
The compiler proved the types here and still emitted the slow path.
This is a missed optimization in Bun, not a problem in your code. Please report it and include the entry.
builtin-result#
The value is the result of a built-in function whose result type the compiler doesn't know.
Narrow the result.
runtime-code#
The value comes from code that isn't in the executable.
const plugin = await import(process.argv[2]);
const options = plugin.options;
const options = readOptions(plugin.options);
console.log(options.port);Keep eval'd code and modules loaded at run time away from this value. Where you can't, build a new object from it, as here.
unchecked-value#
A value that nothing checked against its declared type can reach this access.
The entry's why line says where the value got past TypeScript. Why a guard is there has a section for each code.
caught-value#
The value was caught, and anything can be thrown.
try {
run();
} catch (error) {
console.log(error.message);
if (error instanceof Error) console.log(error.message);
}Narrow it first, for example with instanceof Error.
called-by-unknown-code#
A function is handed to code the compiler can't see here, which can call it with any arguments.
The entry points to where the function, or an object that holds it, is handed over. Everything that the function does with its parameters counts for that line.
const plugin = await import(process.argv[2]);
plugin.onOrder(handleOrder);
plugin.onOrder((body: unknown) => handleOrder(readOrder(body))); Call the function from your own code, or hand over a small wrapper that checks its arguments.
read-from-shared-object#
An object is handed to code the compiler can't see here, which can write anything into it.
The entry points to where the object is handed over. Everything that's read from it afterwards counts for that line.
Hand over a copy, or only the fields that code needs.
unknown-callee-result#
The value is returned by a call whose callee isn't known at build time.
Give the callee a function type.
no-fixed-layout#
The object is made by a function that the compiler found no fixed layout for.
Use a class or an object literal.
lost-track#
The compiler stopped tracking the objects that reach this access.
Give the value a declared type, or split the function.
Why a guard is there#
Typed code trusts its declared types. Where the build sees every caller and each one passes the declared types, nothing is checked at run time. Otherwise a guard at the function's entry checks the arguments, and Bun also compiles a slow version of that function, which works like plain JavaScript on the value that didn't match. If an argument doesn't match its declared type, the call takes the slow version. The result is the same as under bun run. It's just slower.
The fast version needs no guard where every value that can arrive was created as its declared type, or has already passed a guard. TypeScript lets other values through in a few places. The why line of an entry says which one applies.
| Code | The value, object or function |
|---|---|
| unclassified | Has no recorded reason |
| loose-type | Comes from any, unknown, object, {}, Function or a bare type parameter |
| unsound-cast | Went through an as cast |
| suppressed-error | Is behind a suppressed type error |
| declaration-only | Has a type from a declare or a .d.ts file |
| external-data | Comes from outside the program |
| unexpected-shape | Has a shape its declared type doesn't include |
| not-a-plain-object | Is a primitive, function, array, built-in object or proxy |
| wrong-type-stored | Is read from a field that holds a value of a different type |
| computed-key-store | Is read from a field that a store with a computed key may write |
| store-through-unknown-object | Is read from a field that a store through an untyped object may write |
| passed-to-builtin | Is passed to a built-in function that may write to it |
| object-changed | Is changed after it's created |
| written-by-runtime-code | Can be written by code that isn't in the executable |
| called-by-runtime-code | Can be called by code that isn't in the executable |
| unknown-callee | Is returned by a callee that isn't known at build time |
| other-this | Can run with a different this |
| called-by-builtin | Is called by a built-in |
| untyped-collection | Is read from a collection that something else may add to |
| element-may-be-missing | Is read from an index that may be out of bounds |
| direct-eval | Is a variable that a direct eval() can reassign |
| read-before-init | Can be read before it's initialized |
| lookup-may-change | Is a method that can be replaced at run time |
Eight codes are also causes. Six have their section there. external-data and unexpected-shape have theirs below.
unclassified#
The compiler recorded no reason for this guard.
This is a bug in Bun. Please report it and include the entry.
loose-type#
The value comes from 'any', 'unknown', 'object', '', 'Function' or a bare type parameter.
function total(order: any) {
function total(order: Order) {
return order.price * order.count;
}Narrow it, or give the source a precise type.
unsound-cast#
An 'as' cast changes the type here, and nothing checks it.
const order = body as Order;
const order = readOrder(body); Remove the cast, or narrow the value first. A cast to a wider type, such as circle as Shape, costs nothing.
suppressed-error#
A type error is suppressed here (@ts-ignore, @ts-expect-error, @ts-nocheck).
// @ts-expect-error
order.count = "2";
order.count = 2; Fix the type error.
declaration-only#
The type comes from a 'declare' or a .d.ts file, which the compiler can't check against the code.
declare function loadOrder(id: string): Order;
const order = loadOrder(id);
const order = readOrder(loadOrder(id)); Narrow the value, or compile the typed source of the package. Publish a library for bun compile shows how a package ships it.
external-data#
The value comes from outside the program (JSON.parse, response.json(), structuredClone, a message).
const order: Order = await response.json();
const order = readOrder(await response.json()); Check its fields, and build a new object of this type from them. unknown-object shows how.
unexpected-shape#
An object reaches this point with a shape its declared type doesn't include.
const shape = { side: 2 } as unknown as Circle;
const shape: Circle = { radius: 2 };
console.log(shape.radius);Trace the object back from the entry's location. An unsound cast hides it from TypeScript.
store-through-unknown-object#
A store through an object of unknown type may write this field.
function reset(target: any) {
function reset(target: Counter) {
target.count = 0;
}Give that object a type.
object-changed#
A delete, Object.defineProperty, Object.setPrototypeOf or Object.freeze changes an object of this type.
interface Session {
user: string;
user?: string;
}
delete (session as any).user;
delete session.user; Declare the property optional, or create the object in its final form. The entry's own code says which change it is: field-deleted, property-redefined, prototype-changed or frozen.
written-by-runtime-code#
Code that isn't in the executable can write to this object.
const plugin = await import(process.argv[2]);
plugin.register(counter);
plugin.register(counter.count); Keep the object out of eval'd code and modules loaded at run time.
called-by-runtime-code#
Code that isn't in the executable can call this function with any arguments.
const plugin = await import(process.argv[2]);
plugin.onOrder(handleOrder);
plugin.onOrder((body: unknown) => handleOrder(readOrder(body))); Keep the function out of eval'd code and modules loaded at run time. Here only the small wrapper is passed in, and handleOrder is only called from compiled code.
other-this#
The method can run with a different 'this' (call, apply, bind, or read off its object and called later).
const reset = counter.reset;
const reset = () => counter.reset();
setTimeout(reset, 1000);Call it as a method, or use an arrow function.
called-by-builtin#
A built-in calls this function (a callback, getter, toString or iterator), so its arguments aren't proven.
orders.sort((a: Order, b: Order) => a.total - b.total);There's nothing to fix if the guard passes. It costs a few instructions.
element-may-be-missing#
The index may be out of bounds, or the array may have holes, so the read can produce undefined.
for (let i = 0; i <= items.length; i++) total += items[i].price;
for (const item of items) total += item.price; Check the index, or loop with for...of.
direct-eval#
A direct eval() in this scope can reassign the variable.
function run(code: string) {
let count = 0;
eval(code);
(0, eval)(code);
return count + 1;
}Use indirect eval, (0, eval)(code), or move the eval out of this scope.
read-before-init#
The field or variable can be read before it's initialized.
class Job {
retries: number;
constructor() {
this.log();
this.retries = 3;
this.log();
}
log() {
console.log(this.retries);
}
}Initialize it in the declaration, or first in the constructor.
lookup-may-change#
The method can be replaced, shadowed or overridden at run time, so the call isn't resolved at build time.
class Counter {
count = 0;
reset() {
this.count = 0;
this.count = 1;
}
}
Counter.prototype.reset = function () {
this.count = 1;
}; Don't assign to methods or prototypes after the class is defined.
More reports#
--explain takes a value for other reports, and you can pass it more than once. It works with bun compile, bun build --compile --aot and bun check --aot. In Bun.build, compile: { aotExplain: "guards" } adds the lines to result.logs. It takes an array too.
--explain=guards lists every type guard that can fail at run time, why the compiler couldn't remove it, and how to fix it. --explain=guards-json prints the same list, one JSON object per line. Find the guards that fail walks through it.
note: 2 type guards can fail at run time. When one fails, the function falls back to its slow path.--explain=trim lists the Bun and Node.js APIs your program uses, and why --trim kept or removed each feature.
aot: trim: blocked by dynamic-import: the program calls import() or require() with a dynamic path at src/plugins.ts:14:10--explain=missed-optimizations lists hot calls that weren't inlined or resolved, loops that weren't optimized, and why.
aot: missed: not-inlined at src/render.ts:41:12 in renderRow, 38 bytes over the inlining budget--explain=allocations lists where each object is allocated (stack, heap or removed), and why.
aot: allocation: object at src/parse.ts:88:18: heap: Return (it can outlive the call that creates it: it is returned)--explain=statistics prints the compiler's counters, one per line. They're for working on the compiler, and their names change between versions.
SourceLines 18204