Typescript coding style
Personal Agent Skills
npx -y skills add jtmthf/skills --skill typescript-coding-styleAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Applies Jack Moore's personal TypeScript/TSX coding style — derived from his own pre-2025 open-source code and gists, not a generic style guide. Use this whenever writing, generating, refactoring, or reviewing TypeScript or TSX code: new functions, modules, types, React components, hooks, API handlers, utility libraries, or any code-generation task in a TS/TSX file. Trigger even when the user doesn't explicitly ask for "my style" — it should shape ordinary TypeScript work by default. Key signals: declarative composition over loops, object-param destructuring instead of positional args, plain factory functions over classes, discriminated unions and exhaustive `match` for state, errors modeled as values (Result/Option) instead of scattered try/catch, erasable syntax (no enums/access modifiers/decorators), fully spelled-out identifiers, and minimal comments. Always defers to a project's existing conventions first (lint config, prevailing patterns, framework requirements like NestJS decorators) — this skill fills gaps and greenfield code, it doesn't override established codebases.
The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
11.9 KB, as published. Nobody here has run it
TypeScript Coding Style — Jack Moore
This encodes how Jack actually writes TypeScript, reverse-engineered from ~35 of his pre-2025 gists and personal repositories (utility libraries, a Result/Option library called outcome, NestJS services, Next.js apps). It is not a generic best-practices list — every rule below has a citation in his own code, and the exceptions are just as load-bearing as the rules.
Priority: existing conventions win
Before applying anything below, check what the project already does:
- Lint/format config (
.eslintrc,biome.json,prettierconfig,tsconfig.jsonstrictness) always wins over personal preference. - Prevailing patterns already in the file or module you're editing win. If the surrounding code uses classes, positional args, or try/catch, match it rather than introducing a jarring one-off in Jack's style.
- Framework requirements are not violations. NestJS demands
@Injectable()/@Controller()classes and constructor DI; Angular demands classes; a library you're porting or extending may demand inheritance. Follow the framework in the framework-mandated parts (decorators, class shape, DI wiring) — Jack does this too — and apply his style in the parts the framework doesn't dictate: method bodies, DTOs/helpers, business logic, error handling.
Apply this skill's style when: writing new files, filling gaps with no established local convention, or the user asks for "my style" / "your style" / "the way you'd write it."
Core style
1. Declarative over imperative. Prefer composing map/filter/flatMap/reduce, generator pipelines, or a pipe/flow chain over manual index loops and mutation. Use type-guard predicates (pickBy(obj, isNonNullable)) instead of hand-rolled null-filtering loops.
2. Object params over positional args. Any function taking more than ~2 parameters — and especially any "options" or domain payload — takes a single destructured object parameter, with defaults applied in the destructuring:
function useEventSource<T = string>(
url: string,
{ withCredentials, extractor = JSON.parse }: UseEventSourceOptions<T> = {},
) { /* ... */ }
An identifying key (an id, a slug) can stay positional alongside a trailing options object — updateArticle(slug, { title, body }) is fine. The rule is "domain payloads as objects," not "zero positional args ever."
3. Plain objects and functions over classes — with two real exceptions. Default to factory functions returning object literals, not classes. Reach for a class only when:
- modeling a stateful collection/data structure, especially one that implements a standard interface (
Iterable<T>,AsyncIterable<T>,Map<K, V>) — implement these standard interfaces proactively when it makes the type more portable/interoperable, even if nothing forces it. Don't force-fit an interface that doesn't match the domain just because it's available (e.g. a task queue is genuinelyIterable, but isn't aMap), or - deliberately porting/mirroring an external API's design (e.g. a TS port of a Java library keeps that library's class hierarchy on purpose).
Outside those two cases, a class is a smell. Prefer composition of small functions and plain object state. The same instinct applies one level down: prefer standard platform primitives over bespoke equivalents for cross-cutting concerns — AbortController/AbortSignal for cancellation, EventTarget for pub/sub — rather than inventing a custom cancel-token or emitter shape.
4. Erasable syntax. No enum (use string-literal unions), no public/protected/private access modifiers, no experimental decorators. When a field genuinely needs encapsulation, use a JS #private field, not the TS private keyword. This is a habit that solidified in his 2022+ code — weight newer patterns over older ones when they conflict.
5. Make impossible states unrepresentable. Model state with discriminated unions and narrow with type guards or an exhaustive match({ ... }) visitor rather than a "bag of optionals" (a single type with several ? fields that admits invalid combinations no valid state actually has). Tag the discriminant field type (or value for state-machine states) — not kind. When multiple branches repeat the same properties, don't repeat the literal — factor the shared fields into a named base type and compose per branch with an intersection (BaseContext & { user: User; error: undefined }); that per-branch intersection is idiomatic and different from the type-vs-interface call in rule 11. See references/examples.md for the canonical Result/Option and state-machine shapes.
6. Spell everything out. No abbreviated identifiers — previous/accumulator/predicate/producer, never prev/acc/pred/fn. This holds even for loop variables and generic callback params.
7. Minimal comments; JSDoc/TSDoc when behavior isn't self-evident. Code should read as self-explanatory from naming and types by default. Reserve comments for genuinely public/exported APIs and for the cases where intent isn't obvious from the signature — be concise, and use the {@link otherThing} tag to cross-reference related functions/types in the same file rather than re-explaining them.
8. Errors as values; try/catch stays narrow and centralized. Model fallible operations as a Result<T, E> — prefer a named object shape ({ ok: true; value: T } | { ok: false; error: E }) over a positional [error, value] tuple; if a tuple is ever genuinely warranted, its positions should still be named via labeled tuple elements. Throwing is fine where it produces a desirable outcome in the surrounding system (e.g. a framework request handler that expects to catch and translate exceptions) — the rule is about avoiding manual try/catch at ordinary call sites, not eliminating throw everywhere. When a try/catch is genuinely needed, wrap it once at that single boundary and convert it into a value immediately — don't let try/catch spread through calling code:
function tryCatch<TValue>(producer: () => TValue): Result<TValue, unknown> {
try {
return { ok: true, value: producer() };
} catch (error) {
return { ok: false, error };
}
}
Errors that carry a caught cause should stay real Error instances (or a subclass) so stack traces survive — wrap with new Error(message, { cause: originalError }) rather than discarding the original or re-throwing a plain string/object.
9. Invariants upfront, not defensive code. Guard clauses and early returns (or an invariant() call) at the top of a function, then a straight-line happy path — not scattered null checks deeper in the body. unwrap()/expect()-style APIs fail fast on the impossible branch instead of quietly tolerating it. Throw the most specific applicable error — a native subclass (RangeError, TypeError) or a purpose-built one (InvariantError) — rather than a generic new Error(...), so callers and stack traces carry real signal. Model "must always be considered, but may be absent" fields as field: T | undefined rather than field?: T — the explicit form forces every caller to consciously decide what to pass instead of silently omitting it.
10. Strict at the boundary, pragmatic inside — but never pragmatic about untrusted data. Public and domain-facing APIs stay fully typed — discriminated unions, type guards, no any. Inside a generic function's body, any is genuinely the right tool when TypeScript can't line up your runtime logic with your type-level logic (a conditional return type is a common trigger for this) — that's a different situation from casting to shortcut real work. Never use as to force a shape onto external or untrusted input (API responses, form data, env vars, JSON.parse output) — validate it with a schema (e.g. zod's safeParse) and let the parsed result carry its real inferred type. Prefer unknown over any for genuinely-unknown external input. Prefer undefined for absence in normal code; reserve null for wire formats (JSON/GraphQL) where null is the actual serialized value.
11. type over interface by default. Reach for interface only when you need declaration merging, or when composing several object shapes together — interface extends has meaningfully better compiler performance than a multi-way & intersection, so prefer it for that specific case. This is distinct from the per-branch discriminated-union pattern in rule 5 (BaseContext & { ... }), which is fine as an intersection because it's narrowing one union member, not building up a shared contract.
Additional conventions
A few more mechanical rules, folded in from direct review feedback and cross-checked against his code — apply these alongside the core style above:
- Braces always. Every
if/for/while/etc. body gets braces, even a single statement. async/awaitover explicitPromiseconstruction. Preferawaiting overnew Promise(...)/.then()/Promise.resolve()/Promise.reject()chains when the code can be written that way.- Generic type parameters default to a descriptive
T-prefixed name —TValue,TError,TKey,TContext— rather than a single bare letter. A bare letter (T,K,V) is only acceptable for a minimal, single-parameter, self-evident utility (Array<T>-shaped signatures). readonlyby default on object type properties; omit it only when the property is genuinely mutable.- Declare explicit return types on top-level exported functions (skip this for components returning JSX — the return type is always JSX and adds no signal).
- Enums: never introduce a new one — use a string-literal union, or an
as constobject pluskeyof typeof/(typeof x)[keyof typeof x]when you need an enum-like value/label mapping. (Numeric enums are worth actively avoiding: they generate a reverse mapping, so a 4-member enum produces 8 object keys — a common source of confusion.) If a project already has enums, the "existing conventions win" priority above still applies — don't rip them out uninvited. satisfiesover a type annotation oraswhen you want to check a value against a type while keeping its narrower inferred type (literal values, tuple shapes) intact.- Be aware some projects enable
noUncheckedIndexedAccessintsconfig.json, which makes array/object index access returnT | undefinedinstead ofT. Check for it before assuming an indexed read is safe without a guard.
Further reading
references/patterns.md— naming conventions, exports, generics, function-overload style, file naming, formatting, and testing conventions.references/examples.md— canonical code snippets for each principle above, pulled from Jack's own repositories, to pattern-match against when generating new code.