{"slug": "freerange-static-fit-checks-for-ordinary-typescript-layout-code", "title": "Freerange: Static fit checks for ordinary TypeScript layout code", "summary": "Freerange, a static analysis tool built on the official TypeScript API, lets developers find potential NaN, Infinity, division by zero, and out-of-bounds array indexes in TypeScript code at compile time without annotations or library functions. The tool, which uses a negligible fraction of TypeScript's analysis time, is designed to help AI agents guarantee UI layouts without touching the browser and supports console.assert for static contract checking.", "body_md": "Freerange shows you the range of every `number`\n\nin your TypeScript codebase, letting you find potential `NaN`\n\n, `Infinity`\n\n, division by zero, out-of-bounds array indexes, and more.\n\n**Uses the official TypeScript API**. Not a new language, not a fork. No annotations, no library functions.** Static**. Freerange works at compile (build) time, like TS. No need to start your app. AI agents can now guarantee UI layouts without ever touching the browser!**Fast**. Uses a negligible fraction of TypeScript's analysis time.** Robust**. Adversarially tested by agents against thousands of edge cases.\n\nFreerange is deliberately designed to cater to a useful (and growing) subset of TypeScript, and gives concrete guidance for moving important calculations into that subset, so that your code and math can meet in the middle to unlock the most proof power without much ergonomics drawbacks. AI agents are especially well-suited to refactor such code, and we highly recommend you asking them to do so. However, if you/they do find an unsupported TS feature truly valuable, please file an issue!\n\n```\nbun install --dev @chenglou/freerange\n```\n\nThere's no API =). Your TypeScript code provides enough information for Freerange's analysis. We recommend that your agents shape code in the analysis-friendly ways described below.\n\n`fr`\n\n: print project errors and warnings`fr --audit`\n\n: print every function's contracts, plus refactor suggestions to help Freerange analyze better. Great for agents\n\nPass a file path to either command to filter down to just that file's report.\n\n`fr`\n\ndirectly uses TypeScript under the hood, so it naturally respects your `tsconfig`\n\n. We output TS errors before our analysis, so technically, you can swap out your explicit `tsc --noEmit`\n\ncommand for `fr`\n\nand nothing changes!\n\n```\nfunction gridColumnCount(containerWidth: number) {\n  return Math.floor(containerWidth / 240)\n}\n\nfunction gridItemWidth(containerWidth: number) {\n  return containerWidth / gridColumnCount(containerWidth)\n}\n\ngridItemWidth(200)\n```\n\n`bun fr`\n\noutputs:\n\n```\nindex.ts:9:1 - error [inferred-requirement]: call to gridItemWidth violates its nonzero divisor requirement (division at index.ts:6:10)\n```\n\nHow it works: Freerange follows `200`\n\ninto `gridItemWidth`\n\n, then through the call to `gridColumnCount`\n\n. It works out that `Math.floor(200 / 240)`\n\nis `0`\n\n, then catches the later division by that result. TypeScript only knows that these values are numbers; Freerange follows their ranges through both functions.\n\nDid you know that `console.log`\n\nhas a lesser-known sibling: `console.assert`\n\n? When the assertion is true, it stays silent. When the assertion is false, it reports a failure.\n\nBy itself, `console.assert`\n\nisn't as universally useful as `console.log`\n\n. Freerange changes that by analyzing `console.assert`\n\n**statically**:\n\n```\nexport function itemColumn(itemIndex: number, columnCount: number): number {\n  console.assert(Number.isInteger(columnCount))\n  console.assert(columnCount >= 1)\n\n  const index = Math.max(0, Math.floor(itemIndex))\n  const column = index % columnCount\n\n  console.assert(column < columnCount)\n  return column\n}\n```\n\nIn the example above, calling `itemColumn(0, 2.2)`\n\nproduces an error (`columnCount`\n\nshould be an integer) **at compile time**, not at runtime! No need to start a browser to know that the code is wrong here.\n\n`console.assert`\n\ncalls at the very beginning of a function, before any other statement, are caller requirements. Like parameter types, every caller must satisfy them.\nAny `console.assert`\n\nlater in the function will be proven by Freerange for the function itself. Otherwise, Freerange reports an error.\n\nFor simplicity and predictability, `console.assert`\n\ncurrently works only in named top-level functions and accepts simple numeric checks:\n\n`Number.isInteger`\n\n,`Number.isFinite`\n\n,`Number.isNaN`\n\n- Strict comparisons (\n`===`\n\n,`!==`\n\n,`<`\n\n,`>`\n\n,`<=`\n\n,`>=`\n\n) using number literals, object paths, and`array.length`\n\n- References to module constants. For caller requirements, the constant must resolve to a numeric literal\n\nWe also don't support aliasing `console.assert`\n\n, e.g. `const assert = console.assert`\n\n.\n\nFor more complex assertions, like inline calculations, extract them into variables:\n\n``` js\nconst availableWidth = frame.right - frame.left\nconsole.assert(availableWidth >= 0)\n```\n\n(You can strip `console.assert`\n\nin production with bundler or Bun's drop feature, as you may already be doing.)\n\nThere are infinitely many assertable things. Here are some good, non-noisy ones:\n\n- Guarantee that two UI items don't overlap:\n\n```\nconsole.assert(input.bottom < content.top)\n```\n\n- Guarantee that a virtualized list never renders more items than intended:\n\n``` js\nconst visibleItemCount = endIndex - startIndex\nconsole.assert(visibleItemCount <= MAX_VISIBLE_ITEM_COUNT)\n```\n\n- Ensure that two separately calculated values are equal:\n\n``` js\nconst frame = {\n  input: {bottom: inputBottom},\n  inputTray: {bottom: inputBottom},\n}\nconsole.assert(frame.inputTray.bottom === frame.input.bottom)\n```\n\nEvery plain `number`\n\nparameter, including a number field in a fixed-shape object parameter, already requires a finite, non-`NaN`\n\nvalue. Freerange also checks whether a divisor may be `0`\n\nand the other conditions shown by `fr --audit`\n\n. You don't need to assert the same information explicitly.\n\nFreerange supports a subset of TS:\n\n- Named, synchronous top-level functions in a file; Freerange follows calls between functions in the same file\n- Numbers, booleans, strings, nullable values, plain objects, tagged unions, dense arrays, and fixed tuples\n`if`\n\n/`else`\n\n, ternaries, non-fallthrough`switch`\n\n,`&&`\n\n,`||`\n\n,`!`\n\n,`??`\n\n,`for`\n\n,`while`\n\n, and`for...of`\n\nloops- Arithmetic, comparisons, object field and array reads, selected\n`Math`\n\noperations, and`Number.isInteger`\n\n,`Number.isFinite`\n\n, and`Number.isNaN`\n\nFreerange could theoretically support a much larger subset of TS, and did before its public release. Those patterns often made numeric inference and proofs much harder and slower, however, and some questions are undecidable in general. Now that AI agents write code, we strongly recommend asking agents to refactor important calculations into shapes that Freerange analyzes well, guided by `fr --audit`\n\n. Code that is easy to analyze tends to resemble functional programming: immutable data, explicit inputs and outputs, and clean, direct control flow.\n\n-\n**Put important calculations in small named functions.** A React component, callback, or async function can call a plain synchronous helper. Keep any helper functions that Freerange needs to inspect in the same file.\n\n```\nexport function fittedImageHeight(frameWidth: number, imageWidth: number, imageHeight: number): number {\n  const width = Math.max(1, imageWidth)\n  const height = Math.max(1, imageHeight)\n  return (frameWidth * height) / width\n}\n\nfunction ImageCard(props: {frameWidth: number; imageWidth: number; imageHeight: number}) {\n  const height = fittedImageHeight(props.frameWidth, props.imageWidth, props.imageHeight)\n  return <img style={{height}} />\n}\n```\n\n-\n**Name a calculation before checking it.** If the divisor is`oldMax - oldMin`\n\n, write`const oldSpan = oldMax - oldMin`\n\n, check`oldSpan === 0`\n\n, and divide by`oldSpan`\n\n. Checking`oldMin === oldMax`\n\ndoes not tell Freerange about the separately calculated`oldSpan`\n\n. Audit code:`[guard-derived-value]`\n\n. -\n**Decide how invalid inputs should be handled before using them.** If`columnCount`\n\nmust be a positive integer, either require that with leading`console.assert`\n\ncalls or normalize it with`Math.max(1, Math.floor(columnCount))`\n\n. Only normalize when the application actually wants that runtime behavior. Audit code:`[encode-input-rule]`\n\n. -\n**Choose the order of arithmetic deliberately.** Formulas that are equivalent on paper can round, overflow, or underflow differently with JavaScript numbers. For example,`frameWidth / (imageWidth / imageHeight)`\n\nintroduces a ratio that can round to zero;`(frameWidth * imageHeight) / imageWidth`\n\navoids that particular problem. The two expressions can still round differently. Audit code:`[use-direct-operands]`\n\n. -\n**Decide what a missing array element means.** Use`values[index] ?? fallback`\n\nonly when the application really wants a fallback. Otherwise, prove that`index`\n\nis an integer from zero through`values.length - 1`\n\nbefore using`values[index]!`\n\n. A bounds check cannot detect a hole in a sparse array, so Freerange expects arrays to be dense. Audit codes:`[handle-missing-element]`\n\n,`[guard-array-index]`\n\n. -\n**Write the condition and loop directly.** Prefer`width === 0`\n\nover using a number as a condition, such as`width || 1`\n\n. Use a regular loop for a simple dense-array calculation when callback arguments, callback effects, and a newly allocated result array do not matter. Audit codes:`[write-explicit-condition]`\n\n,`[use-loop-for-aggregation]`\n\n. -\n**Write object copies explicitly.** Use`{width: layout.width, height: layout.height}`\n\ninstead of`{...layout}`\n\n. Use dense arrays and fixed-length tuples, and do not modify objects or arrays after creating them. Rebuilding an object is not equivalent to mutation when other code observes its identity or the mutation. -\n**Give each union case a tag and switch on it.** Use a tagged union when different cases carry different fields. Make the`switch`\n\nexhaustive and do not use fallthrough. -\n**Check and use the same local value.** When a value comes from module, class, or reactive state, store it in a local first. For example, write`const currentScale = scale; if (currentScale !== null) return currentScale`\n\ninstead of checking one read of`scale`\n\nand returning another. -\n**Use precise TypeScript types.** Avoid`any`\n\n, casts, and suppression comments. Parse external data before passing it to a numeric helper, give the helper typed parameters, and pass only the fields it uses. A file containing`@ts-ignore`\n\n,`@ts-expect-error`\n\n,`@ts-nocheck`\n\n, or`eval`\n\nis rejected because its declared types cannot be trusted.\n\nThe only TypeScript compiler option that Freerange mandates is `strictNullChecks`\n\n(otherwise the analysis is too unsafe), which is enabled when `strict`\n\nis on. We generally recommend enabling the options below as well. They aren't necessary for Freerange's analysis, but they help AI agents and humans write safer code that is more likely to be analyzable:\n\n```\n{\n  \"compilerOptions\": {\n    \"strict\": true,\n    \"noImplicitAny\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"exactOptionalPropertyTypes\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true\n  }\n}\n```\n\nFreerange uses a few terms consistently:\n\n`requires`\n\n: a condition the caller must satisfy. The function's guarantees assume the condition is true.`ensures`\n\n: a guarantee about the returned value whenever the function returns.`assumes`\n\n: an input condition Freerange accepts without proving, such as an array being dense or every element of a`number[]`\n\nbeing finite.`proves`\n\n: a successful static`console.assert`\n\ncheck.`unsupported`\n\n: Freerange cannot analyze the function because it uses code outside the analyzed subset. Freerange shows the first blocker you can potentially refactor.`partially supported`\n\n: Freerange can analyze some, but not all, of the function.`skipped`\n\n: some top-level statements in the modules weren't analyzed.\n\nA caller requirement is not automatically a bug. For example, `requires: columns >= 1`\n\nmeans the function is safe under that condition; it does not mean Freerange found a caller passing zero. Freerange checks supported same-file calls, but it is not a repository-wide call-site verifier. Imported calls and unsupported callers may remain unchecked.\n\nAn `ensures`\n\nline assumes its `requires`\n\nand `assumes`\n\n. A requirement may be a real API rule, or it may expose a relationship Freerange cannot currently prove. An assumption may identify a real input boundary or an analysis limitation. Decide what the program should do before changing code to remove either one.\n\nAlways read the coverage line. No findings does not mean an unsupported file is safe. A derived guarantee becoming weaker, for example `at least 54`\n\nbecoming `at least 0`\n\n, appears in the audit rather than the shorter findings output.\n\nIf your production code actually needs support beyond these limits, please file an issue! We're open to relaxing the limits.\n\nFreerange's numeric analysis is designed around arithmetic used in layouts and other everyday application code. For each TypeScript `number`\n\n, Freerange tracks one continuous range, whether the value is an integer, whether it may be `NaN`\n\nor infinite, and at most one exact number that a branch proved impossible. For example, after `value !== 0`\n\n, Freerange can remember that `value`\n\ncannot be `0`\n\n.\n\nFreerange does not keep separate ranges or arbitrary sets of possible numbers. If one branch produces `1..2`\n\nand another produces `10..11`\n\n, Freerange keeps the combined range `1..11`\n\n. A later check that rules out a different exact number may replace the number remembered from an earlier check.\n\nFreerange recognizes repeated uses of one already-computed value, including aliases, stable property and array reads, lengths, and duplicate arguments passed to a supported function in the same file. For example, `const span = right - left; span - span`\n\nis exactly `0`\n\nunless `span`\n\nis infinite or `NaN`\n\n. Two separate evaluations are not assumed equal, even when their source code looks the same; store the result in a local when that identity matters.\n\nEarlier versions included an exact rational linear prover based on Farkas' lemma and other relational machinery. The current analyzer does not use that machinery or an SMT solver. In practice, those approaches made analysis unpredictable without proving much more real-world code. We also decided against analyzing numbers as real numbers, which would have been sweet, because doing so produces false proofs: floating-point arithmetic is not associative, rounds results, and can overflow or underflow.\n\nFreerange follows calls to supported functions in the same file using the ranges known at each call. Imported functions are not followed. Imported constants are followed only when they resolve to a numeric literal such as `export const GAP = 24`\n\n. Literal default parameters work in supported calls; object and calculated defaults do not. Passing more arguments than the implementation declares is also unsupported. Unknown calls, when callbacks run, caught exceptions, changes made through another reference to the same object, and most framework behavior remain outside the subset.\n\nInside a `console.assert`\n\n, Freerange can follow common UI calculations through named values: `Math.min`\n\nand `Math.max`\n\n, addition or subtraction by a nonnegative value, the same multiplication by a nonnegative value on both sides of a comparison, the fact that `index % columnCount < columnCount`\n\nwhen `columnCount`\n\nis positive, and fields read from a freshly constructed record. Freerange does not chain arbitrary comparisons: `left <= middle`\n\nand `middle <= right`\n\ndo not by themselves prove `left <= right`\n\n.\n\nFreerange analyzes a loop again until the ranges known at the start of an iteration stop changing. Freerange does not simulate every runtime iteration or try to produce a formula for the final value. Ordinary counting loops usually settle after two or three analysis passes. If the ranges still change after 16 passes, analysis stops for that path.\n\nFreerange follows records, tuples, arrays, and tagged unions declared in your project through at most eight nested levels. A deeper property becomes unknown. A function is unsupported when its top-level input type cannot be represented.\n\nProperty reads are assumed to be stable and side-effect-free during one analyzed function call. A getter or Proxy that changes its answer or performs work is outside the model. Property writes, including assignments that invoke setters, are unsupported. Object spread is also unsupported because JavaScript copies only an object's own enumerable properties, which may not match the fields declared by its TypeScript type.\n\nEvery plain `number`\n\nparameter must be finite and not `NaN`\n\n. The same rule applies to number fields in fixed-shape object parameters, even when the function does not read them. Numeric literal types such as `1 | 2`\n\nalready satisfy the rule. Nullable numbers, arrays, tuples, and tagged unions keep their more specific `assumes`\n\nlines instead. A supported literal default is used when an argument is omitted, so the default can satisfy the requirement automatically.\n\nCalls to supported functions in the same file either prove these requirements, pass them outward to their own callers, or report a definitely invalid argument. A successful call also proves that the same stored argument is finite afterward. Writing `console.assert(Number.isFinite(value))`\n\nat the start of the function is allowed but normally redundant; `Number.isInteger(value)`\n\nis stronger and replaces the finite requirement.\n\nDivision and array reads can create additional requirements. Freerange tries to express each one using the function's parameters so callers can be checked. A later operation on the same stored value and path may rely on that requirement; assigning a new value or repeating the calculation starts over. Freerange follows each intermediate calculation at most once. If one pass cannot reach the parameters, Freerange prints a local `assumes`\n\ncondition instead.\n\nThese limits may make a result less precise or stop analysis, but they cannot make a guarantee stronger than the code supports.\n\n```\nbun install\nbun run check\n```\n\n", "url": "https://wpnews.pro/news/freerange-static-fit-checks-for-ordinary-typescript-layout-code", "canonical_source": "https://github.com/chenglou/freerange", "published_at": "2026-07-21 01:55:30+00:00", "updated_at": "2026-07-21 02:23:11.492800+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": ["Freerange", "TypeScript"], "alternates": {"html": "https://wpnews.pro/news/freerange-static-fit-checks-for-ordinary-typescript-layout-code", "markdown": "https://wpnews.pro/news/freerange-static-fit-checks-for-ordinary-typescript-layout-code.md", "text": "https://wpnews.pro/news/freerange-static-fit-checks-for-ordinary-typescript-layout-code.txt", "jsonld": "https://wpnews.pro/news/freerange-static-fit-checks-for-ordinary-typescript-layout-code.jsonld"}}