Guardrails

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.

Rejected ts
if (value) {
}

if (name) {
}

while (items.length) {}
Allowed ts
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.

Why this helps LLMs

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.

Inspired by Haxe.

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.

Rejected ts
switch (state) {
  case "idle":
    logState(state);
  case "ready":
    return "stable";
  default:
    return "other";
}
Allowed ts
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.

Why this helps LLMs

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.

Inspired by MISRA.

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.

Rejected ts
function normalize(user: User): User {
  user = process(user);
  return user;
}
Allowed ts
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.

Why this helps LLMs

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.

Inspired by MISRA.

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.

Rejected ts
const y = -true;
const y = "5" - 2;
Allowed ts
const y = -1;
const parsedValue = Number.parseInt("5", 10);
const result = parsedValue - 2;

This applies to operators such as -, *, /, %, and unary numeric operators.

Why this helps LLMs

Typed arithmetic prevents the model from leaning on JavaScript coercion folklore. The generated code has to make the conversion step visible, reviewable, and testable.

Inspired by Haxe.

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.

Rejected ts
let payload: any = readPayload();
value!.name;
Allowed ts
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.

Why this helps LLMs

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.

Inspired by SPARK.

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.

Rejected ts
if ((x = y)) {
}
Allowed ts
x = y;
if (x !== undefined) {
}

The same rule applies consistently across condition positions such as if, while, do while, and for (...; condition; ...).

Why this helps LLMs

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.

Inspired by ADA.

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.

Rejected ts
if (value == 0) {
}

if (name != null) {
}
Allowed ts
if (value === 0) {
}

if (name !== null) {
}

There is no special exception for x != null. The rule stays simple: no ==, no !=.

Why this helps LLMs

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.

Inspired by Elm.

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.

Rejected ts
class MathObject {
  add(left: number, right: number) {
    return left + right;
  }
}
Allowed ts
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.

Why this helps LLMs

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.

Rejected ts
async function syncUser(user: User): Promise<void> {
  fetchProfile(user.id);
}
Allowed ts
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.

Why this helps LLMs

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.