| name | typescript-as-go |
| description | Enforce agents to write TypeScript as if it were Go; simple, explicit, boring |
| license | MIT |
| compatibility | Designed for Claude Code (or similar products) |
| metadata | {"author":"revett","repo":"https://github.com/revett/typescript-as-go","version":"0.2.0"} |
TypeScript As Go
Task
Write TypeScript as if it were Go: simple, explicit, boring. When you are unsure how to write
something, ask what the dullest Go programmer would do, and do that. The goal is code that is fast
to read and review, not clever to write. No magic.
Rules
As an agent working in this project, you must follow the following allowed and banned rules when
writing Typescript.
Allowed
- Pure functions first, reaching for a class only where a framework contract demands one and
keeping it a shell whose methods delegate immediately to module level functions; prefer
composition over inheritance
- Named exports only, using a default export solely when a framework requires it
- Small files with one concern each and kebab-case file names (
settings-tab.ts), graduating to
package folders (settings/tab.ts) only once a concern outgrows a single file
- Framework code stays thin glue, with logic living in pure modules that never import the framework
- Names short and evocative, scaled to scope (terse for locals, descriptive for exports), never
prefixing a getter with
get (owner(), not getOwner()), always camelCase or PascalCase and
never snake_case
type over interface for data shapes, since they are structs not contracts and a type cannot
be reopened by declaration merging; use interface only when a framework contract leaves no
choice
- String literal unions for closed sets of values (
type Status = "open" | "closed"), keeping the
syntax erasable at build time
- Keep types small (one or two members) and accept the narrowest shape a function actually uses,
since structural typing means callers need not name the type
- Model variant data as a discriminated union with a shared literal tag, branching on the tag with
a
switch that ends in a default asserting the union is exhausted (assertNever(x))
- Use
satisfies to check a value conforms to a type at compile time without widening it, the
equivalent of Go's compile time interface check
- Explicit zero values, where every type ships a complete default (a
DEFAULT_X constant) usable
as is without further initialization, never undefined shaped holes
- Distinguish absent from zero with an explicit presence check (
map.has(k), k in obj,
x === undefined), never reading a falsy default as "missing"
- Errors are values, so domain logic returns a result the caller inspects (a tuple or
{ ok, value }), never a sentinel like -1, NaN, null, or "" folded into the normal
return
- Give a modeled error structure rather than just a string, carrying the operation, offending
input, and any cause as fields, with lowercase messages that have no trailing period and are
prefixed with their origin (
settings: unknown theme "$name")
- Guard clauses and early returns, since flat beats nested
- Braces on every
if, even a one line body
- Release resources with
try/finally or a using declaration placed right where the resource is
acquired, so cleanup sits beside setup and cannot be forgotten on an early return
- One sentence
// doc comment above every exported symbol, Go style
(// normalizeSettings returns ...)
- Table driven tests with
node:test and node:assert/strict and no test framework dependency,
each test sitting beside the code it covers as name.test.ts, mirroring Go's _test.go pattern
- Reach for a small, focused dependency when hand rolling something fiddly to get right (request
signing, a mock server), but hand roll the moment it drags in a framework or SDK you do not need
- Separate a trailing
return from the block above it with a blank line, so the final result of a
function reads as its own beat rather than crowding the guard or loop that precedes it
Banned
- No enums, use string literal unions instead
- No ternary expressions, and no defaulting through
?? or ||; write the if, or reach for a
file local helper such as stringOr(v, fallback) or numberOr(v, fallback), so the fallback is
an explicit branch and not folded into an operator (|| stays fine inside a boolean condition)
- No
else after a return
- No
switch case fallthrough, every case ending in return or break, with stacked labels to
share a branch
- No throwing for expected failures, returning the error as a value and converting to a result at
the framework boundary, reserving
throw (Go's panic) for impossible states and programmer
errors and never exposing it across a module boundary as an API
- No swallowed errors and no floating promises, never discarding an error or unhandled rejection
you could handle
- No sentinel or in band return values (
-1, null, NaN, "") to signal failure or absence
- No
interface for plain data shapes
- No JSDoc
/** */ blocks, use // comments
- No barrel files and no
utils.ts (or any grab bag dumping ground)
- No inheritance for code reuse (
extends chains), compose instead
- No clever generics
- No decorators
- No magic
- No chained array pipelines (
.filter().map(), .map().filter()) and no .reduce; Go has no
map or filter, so write an explicit for...of that guards with continue and pushes into a
result, one readable loop over a pipeline (a single .map on an already clean list is fine)