simple-typescript
Code Style Preferences
Apply these preferences as an overlay on top of local project conventions. If repository instructions or nearby code clearly require a different pattern, follow the local convention and keep the change consistent.
Core Style
Prefer direct, functional TypeScript.
- Prefer named functions, small modules, and exported plain objects for grouping related operations.
- Do not introduce classes unless there is a strong reason, such as required framework integration, meaningful stateful instances, or an existing class-based local convention.
- Avoid class hierarchies, factory functions, wrapper layers, and ceremony that do not materially improve clarity.
- Optimize for local readability and ease of modification.
- Keep code readable top-to-bottom without forcing readers to jump through many tiny helpers.
Abstractions And Helpers
Avoid abstractions that only rename obvious operations.
- Do not create helper functions that trivially wrap another function, restate syntax, or add a "pretty name" without encoding meaningful domain knowledge — inline those one-liners instead.
- Extract a helper when it removes real duplication, isolates non-trivial behavior, or names a real domain concept such as deriving a status, normalizing an input payload, selecting a canonical item, or computing a baseline value.
- Prefer duplication over a bad abstraction when extraction would hide important logic.
Avoid trivial wrappers — inline instead:
// Don't wrap: const label = parseStringValue(raw.label)
// Do inline: const label = String(raw.label)
Prefer — extract when the helper names a real domain concept:
// Worth extracting: encapsulates non-trivial business logic
function resolveSubscriptionStatus(account: Account): SubscriptionStatus {
if (account.trialEndsAt > Date.now()) return "trial";
if (account.cancelledAt) return "cancelled";
return account.planId ? "active" : "free";
}
Module Boundaries
Keep ownership clear and put reusable infrastructure in shared modules.
- Keep feature-specific files focused on feature-specific logic.
- Put cross-cutting utilities such as HTTP clients, validated request wrappers, object-freezing helpers, and error formatting in shared modules.
- Extract generic helpers only when the same helper appears in multiple places and the abstraction remains obvious.
- Expose concrete named functions.
- Optionally group related operations at the end of a module with a plain exported object.
- Avoid factory functions unless runtime dependency injection is genuinely needed.
- Prefer optional parameters with sensible defaults for simple configuration.
Avoid — factory function for simple configuration:
function createUserService(db: Database) {
return {
getUser: (id: string) => db.find("users", id),
saveUser: (user: User) => db.insert("users", user),
};
}
Prefer — concrete named exports, with an optional grouped object:
export function getUser(db: Database, id: string) {
return db.find("users", id);
}
export function saveUser(db: Database, user: User) {
return db.insert("users", user);
}
// Optional grouping at the end of the module
export const userService = { getUser, saveUser };
Types
Use type by default.
- Use
typefor object shapes, unions, function inputs, and local module types. - Use
interfaceonly for an intentional public contract meant to be extended, implemented, augmented, or consumed across module boundaries. - Keep types explicit enough to communicate domain meaning without adding clever type machinery.
Errors And Recoverable Failures
Prefer explicit tagged errors for recoverable async operations.
- Use result-style return values for I/O, validation, and boundary failures when callers are expected to handle failure.
- Model recoverable failures as discriminated unions with a
tagfield. - Avoid throwing from client or boundary code when the caller can reasonably branch on the failure.
- Throw for programmer errors, impossible states, and failures the current layer cannot handle meaningfully.
- Do not return
nullorundefinedas vague failure signals.
Example shape:
type FetchUserResult =
| { tag: "success"; user: User }
| { tag: "not_found" }
| { tag: "request_failed"; message: string };
Validation And Boundaries
Preserve validation and explicitness.
- Validate untrusted data at external boundaries.
- Fail clearly when external data is invalid.
- Keep validation straightforward; do not fragment schemas or parsing into excessive micro-functions.
- Parse once at the edge, then pass trusted typed values inward.
- Normalize awkward external shapes before they leak into domain code.
Review Checklist
Before finishing a change, scan for style drift:
- Did this add a class, factory, wrapper, or helper without a strong reason? Can any trivial helper be inlined?
- Is reusable infrastructure in a shared module rather than copied into feature code?
- Are module exports concrete named functions, with grouping only where useful?
- Are local shapes modeled with
typeunless an extensible public contract is intended? - Are recoverable I/O and validation failures represented with explicit tagged outcomes, and is validation clear at external boundaries?
- Is runtime validation clear at external boundaries?
More from idrevnii/perks
deep-plan
Structured clarification before decisions. Use when user is in PLANNING mode, explicitly asks to plan or discuss, or when agent faces choices requiring user input. Ensures agent asks questions instead of making autonomous decisions when multiple valid approaches exist or context is missing.
21typescript-engineering
Use this skill whenever the user asks you to write, edit, review, refactor, debug, or design TypeScript or TSX code. It is especially relevant for application code, backend routes, React/UI work, schemas, runtime boundaries, persistence, async workflows, API contracts, tests, lint/typecheck fixes, and code review. Apply it even when the user does not explicitly mention "TypeScript" if the files or project are TypeScript-based.
1