Guardrail Profile
Restrictions that remove ambiguous, high-risk TypeScript and JavaScript patterns.
These guardrails are one half of the broader concept simplification strategy. The structural rules reduce duplicate ways to organize code; the guardrail profile reduces overloaded ways to read expressions.
Conditions must say what they mean
Truthiness is compact, but it is also overloaded. A string might mean “non-empty,” a number might mean “non-zero,” and an object reference might mean “present.” Those are different intentions, so they should be written differently.
if (value) {
}
if (name) {
}
while (items.length) {}if (value !== undefined) {
}
if (name !== "") {
}
while (items.length > 0) {}
Boolean positions should contain a boolean, an explicit comparison, or a logical
combination of boolean expressions. Direct use of string, number,
nullable references, or ambiguous unions is rejected.
This is also a simplification rule. In LLLTS, condition positions are for booleans, not
for a rotating set of truthy interpretations.
Explicit conditions tell the model what property matters: presence, emptiness, count, or a real boolean. That sharply reduces accidental rewrites that preserve syntax but change meaning.
Control flow should not surprise the next reader
switch fallthrough is only allowed when labels are intentionally grouped and the earlier
labels are empty. A case with executable statements must terminate clearly.
switch (state) {
case "idle":
logState(state);
case "ready":
return "stable";
default:
return "other";
}switch (state) {
case "idle":
case "ready":
return "stable";
default:
return "other";
}This rule removes one of the oldest maintenance traps in C-style control flow.
Explicit case termination gives the model a cleaner branch structure. There is no hidden “continue into the next label” behavior to accidentally preserve or introduce.
Parameters are inputs, not scratch variables
Reassigning or incrementing parameters hides data flow. It forces the reader to remember whether a
name still means the original input or a transformed local value. LLLTS keeps that distinction
visible.
function normalize(user: User): User {
user = process(user);
return user;
}function normalize(user: User): User {
const processedUser = process(user);
return processedUser;
}Treat parameters as immutable inputs. If the value changes, give the changed value a new name.
Stable parameter names make the local reasoning graph simpler. The model does not have to decide whether a reused name still refers to the original argument or a later transformed value.
Arithmetic requires numeric intent
JavaScript is comfortable turning values into numbers on demand. Safety-oriented code should not
be. Arithmetic operators in LLLTS are restricted to values that are statically known to
be numeric.
const y = -true;
const y = "5" - 2;const y = -1;
const parsedValue = Number.parseInt("5", 10);
const result = parsedValue - 2;
This applies to operators such as -, *, /, %,
and unary numeric operators.
Typed arithmetic prevents the model from leaning on JavaScript coercion folklore. The generated code has to make the conversion step visible, reviewable, and testable.
Escape hatches stay closed
Two shortcuts are especially damaging in the guardrail profile: any, which disables
checking where precision matters most, and the non-null assertion operator !, which
silences uncertainty instead of resolving it.
let payload: any = readPayload();
value!.name;type Payload = {
name: string;
};
const payload: Payload = readPayload();
if (value === undefined) {
throw new Error("value must be defined");
}
value.name;The preferred style is to model the type you actually expect and to prove non-nullability with control flow.
Both any and ! remove information that a model could otherwise use. Keeping
them out preserves the local proof chain in the code instead of replacing it with trust me markers.
Assignment does not belong inside conditions
Assignment inside a condition is a classic bug source because it compresses two ideas into one expression: mutate something, then decide whether the result counts as true. The rewrite is intentionally more boring. That is the point.
if ((x = y)) {
}x = y;
if (x !== undefined) {
}
The same rule applies consistently across condition positions such as if, while, do while, and for (...; condition; ...).
This helps LLMs because assignment inside a condition creates a dense, high-risk pattern where a
tiny token difference (= vs ==/===) completely changes
control flow. Separating assignment from the condition makes the state change explicit and
leaves the branch as a simple predicate, which is easier for models to generate, review, and
preserve correctly.
Equality must be exact
Loose equality asks the runtime to perform coercion before it compares. That means readers and
tools have to simulate a hidden conversion table just to understand a branch. LLLTS does
not allow that detour.
if (value == 0) {
}
if (name != null) {
}if (value === 0) {
}
if (name !== null) {
}
There is no special exception for x != null. The rule stays simple: no ==, no !=.
Strict equality removes coercion guesses. The model no longer has to infer whether a comparison is about numeric identity, null filtering, or JavaScript conversion behavior.
Return types are part of the contract
If a declared function or method returns a value, its return type must be written at the declaration site. Inference may be convenient, but it hides part of the contract from both readers and tools.
class MathObject {
add(left: number, right: number) {
return left + right;
}
}class MathObject {
add(left: number, right: number): number {
return left + right;
}
}This makes declarations easier to audit mechanically because the contract is complete before you read the implementation.
The model can reason from the signature first. That improves consistency between declaration and body, and it reduces the chance of drifting into an unintended return shape.
Async work must be acknowledged
Promises are easy to create and easy to forget. In safety-oriented code, that is unacceptable. Async work must be awaited, explicitly collected and handled, or wrapped in a clearly documented fire-and-forget pattern if the project chooses to allow one.
async function syncUser(user: User): Promise<void> {
fetchProfile(user.id);
}async function syncUser(user: User): Promise<void> {
await fetchProfile(user.id);
}The rule targets both ignored promises and floating promises inside async flows, where silent failures are especially hard to trace.
Awaiting or explicitly handling promises gives the model a visible sequence boundary. That makes side effects, failure paths, and ordering much easier to preserve during generation.
The broader pattern
Across all of these rules, the language keeps pushing in the same direction: fewer implied conversions, fewer overloaded shortcuts, fewer invisible side effects, and more contracts stated where both humans and tools can see them.
This is why the guardrail profile works well for developer-facing code review and for LLM-assisted authoring. It is not trying to make code shorter. It is trying to make intent harder to fake. The larger cross-cutting framing is documented in Concept Simplification.