Practical, maintainable, performant Typescript.
FAST prioritizes:
- Data-oriented design: separate data from behavior; data structures are simple and serializable
- Separation of concerns: single-purpose functions
- Immutability - easier to reason about and test
- Composition: build complex behavior from simple functions
- Type safety - leverage TypeScript's type system fully
- Functional patterns - transformations over mutations
- Practicality: Mutation and imperative code is allowed within functions for algorithmic simplicity or performance when we own the data and guarantee no external mutation. See Imperative islands for details.
This subset enables better performance (through data-oriented design), easier testing (pure functions), and clearer code (immutability and composition).
- Data is strongly encouraged to be readonly JSON-based (objects/arrays/primitives).
- Other readonly value-like types are also allowed (e.g.
Blob). - TypedArrays/TypedBuffers for high performance interop with WebGPU or other external apis.
const=>(arrow functions)function*(for generators, do not usethis)returnimport,export(ESM)
let(local only; minimal scope)- reassignment (
=) to locals
function
var
type(preferred for all data type definitions)interface(for service interface definitions)readonly- generics
<T>, constraintsextends - conditional types
T extends U ? X : Y inferkeyof,typeof(type queries), indexed accessT[K]as constsatisfiesnever,unknownimport type,export typeexport * as Name(JavaScript re-export pattern for namespace-like organization without runtime objects)
any(never in public interfaces, sometimes in implementations)declare(except.d.ts/ boundary shims)
enum(prefer literal unions)namespace(TypeScript construct that emits non-treeshakeable runtime objects. Useexport * as Nameinstead, which is a JavaScript re-export pattern with no runtime cost)module(use ESM)
- literals
{},[] - destructuring
{ a },[x, y] - spread/rest
... - optional chaining
?. - nullish coalescing
?? - template strings
`${x}`
push/pop/spliceon locally-owned arrays- writing to fields on locally-owned objects
- typed-array/buffer writes for performance (owned locally)
delete(prefer structural copies)- getters/setters
get/set(hidden effects)
- ternary
cond ? a : b - short-circuit
cond && expr,cond || fallback(don't hide effects)
for,for...of,while,do...whilebreak,continueswitch(but data driven maps keyed from discriminated unions preferred)
for...in(preferObject.keys/entries)with(never)
- comparisons:
===,!==,<,>,<=,>= - arithmetic:
+,-,*,/,** - boolean:
&&,||,! - nullish:
?? - bitwise ops as needed:
|,&,^,<<,>>,>>>
- assignment operators:
=,+=,-=,*=,/=,&&=,||=,??= ++,--(local only)
- mutating inputs or captured outer variables
- prototype manipulation
instanceof(prefer tagged unions / branded types)this,superclass/newwhen integrating with libraries/frameworks that require them
- explicit discriminated union error values:
ResultType | ErrorType
try/catch/finallythrowPromiserejection
- exceptions for control flow
Promiseasync/await(treat as effectful; keep deterministic transforms pure)- generators:
function*,yield,yield*
for await...of
- ESM imports/exports
- "no side effects on import" as a default discipline
- DOM/Node IO, timers, random, dates, network, filesystem
- singletons/caches (explicit modules, explicit APIs)
Intent: each data type gets:
- a type-only name (
type MyType = ...) - a value namespace (
MyType.fn(...)) for discoverable utilities - tree-shaking per-utility module in modern bundlers
Folder shape (one type per folder):
<name>.tsis the single entrypoint for that type.- exports the type
Name - exports
* as namespaceNamethat points atpublic.ts
- exports the type
public.tsis the curated surface area (re-exports utilities).- each utility constant lives in its own file (so unused utilities drop out during bundling).
- utility files may contain private declarations but only a single export of same name as file.
Concrete layout:
types/my-type/
my-type.ts # entrypoint: type + export * as <name> from public.ts
public.ts # curated re-exports (public API)
create.ts # one utility per file
create.test.ts
to-bar.ts
to-bar.test.ts
internal-helper.ts # not exported in public
internal-helper.test.ts
Consumer Usage:
import { MyType } from "../types/my-type/my-type.js";
type TypeRef = MyType; // use the type
const value = MyType.create(); // use utility function (via export * as)
const bar = MyType.toBar(value); // most utility functions take type as first argument
Note: The export * as Name pattern provides namespace-like organization without emitting runtime objects. This is different from TypeScript's namespace construct, which emits non-treeshakeable runtime objects and should never be used.
Imperative constructs (mutation, loops, reassignment) are allowed inside a function only when:
- We own the data: the mutated object/array/buffer was created in this function (or is otherwise uniquely owned).
- No one else will mutate: we know no other code can mutate it. This means:
- We export it as
readonlyand release the mutable reference, or - We never export it (e.g., data sent to WebGPU, serialized data, internal buffers).
- We export it as
- No external writes: we don't mutate inputs or captured outer scope.
- No escape: we don't leak partially-built state (no storing references in globals/singletons/caches/long-lived closures).
Heuristic: Mutation is fine only when we have exclusive ownership and guarantee immutability at the boundary (via readonly export and release of mutable reference or no export).