typescript-best-practices
Use whenever writing, editing, refactoring, or reviewing TypeScript or TSX code. This skill defines how TypeScript should be modeled, how boundaries and shared helpers should be handled, how intent should be expressed without code comments, and how to keep code clear, DRY, and free of defensive AI slop. For review workflows, read references/review-and-fix-changes.md.
How do I install this agent skill?
npx skills add https://github.com/justbecauselabs/agent-skills --skill typescript-best-practicesIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides TypeScript coding and review guidelines. It includes a surface area for indirect prompt injection as it processes untrusted user code and has the capability to execute shell commands and modify files. It also uses the jscpd utility via bunx for code analysis.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
TypeScript Best Practices
Use this skill as the default guidance for any .ts or .tsx change.
Keep this file practical. Record the repo's actual TypeScript standards here.
References
- For review or review-and-fix requests on a working tree, branch, commit, or commit range, read
references/review-and-fix-changes.mdand use it as the workflow while keeping this file as the review rubric.
Intent
- Provide a single baseline for how TypeScript should be written and refactored.
- Keep code strongly typed, easy to read, and easy to change.
- Reduce AI slop such as defensive cleanup, vague abstractions, duplicate transforms, and weak fallbacks.
Fast Path
- Validate untyped JSON with Zod at the boundary.
- Keep functions small and composable.
- Keep boundary shapes separate from internal domain shapes.
- Convert external data once, then keep internal code strict and simple.
- Do not normalize values unless there is a real boundary or product reason.
- Preserve original source values; add derived internal keys when needed.
- Use
nullfor meaningful absence in stable domain data. - Reserve
undefinedfor optional inputs and raw boundary payload fields. - Reuse existing helpers before adding new ones.
- For string, number, URL, array, dictionary, dedupe, filter, or similar foundational logic, search for an existing shared helper location in the repo first and add shared logic there instead of burying it in feature code.
- Extract shared invariants and boundary adapters; do not invent generic cleanup utilities.
- Run a quick duplicate-code smoke test with
jscpdafter refactors and before review. - Do not add or expand code comments unless the user explicitly asks for comments in code.
Priority Order
- Follow project-specific conventions first.
- Make boundaries typed and explicit.
- Reuse existing helpers and invariants before adding new abstractions.
- Extract repeated boundary adapters or invariants once repetition is real.
- Prefer clarity over cleverness.
- Apply style preferences only after correctness, boundaries, and reuse are settled.
Core Rules
1. Validate and coerce untyped boundary data once, then keep domain types strict.
Bad:
type Issue = {
id: string;
attemptCount: number;
};
function parseIssue({ body }: { body: string }): Issue {
return JSON.parse(body) as Issue;
}
Good:
import { z } from "zod";
const issueSchema = z.object({
id: z.string().min(1),
attemptCount: z.coerce.number().int().nonnegative(),
});
type Issue = z.infer<typeof issueSchema>;
function parseIssue({ body }: { body: string }): Issue {
return issueSchema.parse(JSON.parse(body) as unknown);
}
2. Keep functions small and composable. Use a params object when a function takes multiple authored parameters.
- Prefer
function name()declarations overconst name = () => {}for authored functions.
Bad:
function buildWorkspaceLabel(issue: Issue, ownerName?: string): string {
const normalizedState = issue.state.trim().toLowerCase();
const normalizedOwner = ownerName ? ownerName.trim() : "unassigned";
const slug = `${issue.identifier}-${normalizedState}-${normalizedOwner}`
.replaceAll(/[^\w-]+/g, "-")
.replaceAll(/-+/g, "-")
.replace(/^-|-$/g, "");
if (!slug) {
return "workspace";
}
return slug;
}
Good:
function buildWorkspaceLabel(params: {
issue: Issue;
ownerName?: string;
}): string {
const stateKey = buildStateKey(params.issue.state);
const ownerKey = buildOwnerKey(params.ownerName);
return buildWorkspaceSlug({
identifier: params.issue.identifier,
stateKey,
ownerKey,
});
}
function buildOwnerKey(ownerName?: string): string {
return ownerName ?? "unassigned";
}
3. Put foundational string, number, URL, array, dictionary, dedupe, and filter logic in shared helpers.
Bad:
function listUniqueIssueIds(issues: Issue[]): string[] {
return [...new Set(issues.map((issue) => issue.id))];
}
Good:
import { unique } from "../shared/collections";
function listUniqueIssueIds(issues: Issue[]): string[] {
return unique(issues.map((issue) => issue.id));
}
If no shared helper exists yet, search for an existing shared helper location first. For purely foundational logic like strings, numbers, URLs, arrays, dictionaries, deduping, or filtering, add the reusable helper there instead of embedding it inside a feature module. Always look for utils/helpers/etc folders for these
4. Do not write code comments unless the user explicitly requests them.
- This includes line comments, block comments, JSDoc, TODOs, commented-out code, and directive comments.
- Existing comments, neighboring style, lint rules, and documentation do not count as explicit user instruction.
- Express intent with precise names, types, small functions, and clear structure.
- Preserve accurate existing comments. If a change makes one stale, remove it instead of rewriting it unless the user explicitly requested code comments.
5. Prefer explicit boundary errors over defensive defaults.
Bad:
function parseLimit(value?: string): number {
return Number(value) || 0;
}
Good:
const limitSchema = z.coerce.number().int().nonnegative();
function parseLimit(value: string): number {
return limitSchema.parse(value);
}
Refactoring Rules
- Before adding a new helper, search the repo for an existing helper, transform, or adapter that already solves the same problem.
- For foundational logic on strings, numbers, URLs, arrays, dictionaries, deduping, or filtering, search for shared helper files first and add that logic there before introducing a feature-local helper.
- If the same invariant, transform, or adapter appears in multiple places, prefer extracting it over adding another copy.
- Extract after real repetition, not because two snippets look vaguely similar.
- Shared helpers should have a clear domain purpose.
- Do not introduce broad
sanitize*,normalize*,clean*, orformat*utilities that mix unrelated concerns. - If an abstraction only exists to generically "clean up" values, the real fix is usually at a typed boundary.
- When refactoring, preserve the strongest existing types. Do not widen types just to make the refactor easier.
- Prefer small boundary adapters and invariant helpers over abstract utility layers.
Duplicate-Code Smoke Test
Use jscpd as a quick duplicate-code smoke test after refactors and before review, like a
typecheck. From the current repository root, run:
cd "$(git rev-parse --show-toplevel)"
bunx --yes jscpd --format typescript --pattern '**/*.ts' --gitignore \
--min-lines 12 --min-tokens 120 \
--max-lines 5000 --max-size 1mb --reporters console --exitCode 0 .
- Keep tests ignored at first because fixtures often create noise; add repo-appropriate ignores or scope the pattern to production code when needed.
- Lower
--min-tokensto60-80when hunting smaller helper duplication. - Use
--reporters html,consolewhen a browsable report would help. - Pair clone detection with targeted
rgfor semantic duplicates: repeated function names, comments, payload fields, or error strings. Exact clone tools do not catch same logic with different spelling.
Code Comment Rules
- Do not add or expand code comments unless the user explicitly requests them.
- Treat JSDoc, TODOs, explanatory blocks, commented-out code, and directive comments as code comments under this rule.
- Do not infer permission from existing comments, neighboring style, lint rules, generated examples, or documentation.
- Preserve accurate existing comments. If changed code makes one stale, remove it instead of rewriting it unless the user explicitly requested code comments.
- Make the code self-explanatory through precise names, strong types, small helpers, and clear structure.
Workflow
- Read this skill before making TypeScript changes.
- For review or review-and-fix tasks, read
references/review-and-fix-changes.mdbefore scoping the work. - Follow project-specific conventions first when they conflict with generic guidance.
- When data crosses into the system as JSON, define a Zod schema at that boundary.
- When values are already trusted and typed, avoid unnecessary cleanup or coercion.
- Keep functions small and composable.
- For functions with multiple authored parameters, usually prefer
fn(params: { ... }). - For functions with a single parameter, pass the value directly instead of wrapping it in a params object unless there is a strong local reason not to.
- Do not add or expand code comments unless the user explicitly requested them.
- If the change is purely about strings, numbers, URLs, arrays, dictionaries, deduping, filtering, or similar foundational types, search for an existing shared helper location in the repo first and add the shared logic there.
- After a refactor, run a quick
jscpdduplicate-code smoke test when the repo/tooling can support it. - If the code seems to require
as, stop and look for a safer typing or validation approach first. - Before adding a new helper or abstraction, search the repo for an existing pattern you should reuse.
- If similar logic exists in multiple modules, prefer extracting a shared invariant or boundary adapter over adding another copy.
- Use subagents to inspect the codebase for existing helpers, repeated transforms, and refactor opportunities before introducing a new abstraction when subagents are available.
- Use subagents for bounded multi-file refactors when the write scope is clear, but avoid broad reusable utilities unless the repetition is real.
- If a proposed abstraction only exists to generically "clean up" values, stop and check whether the real fix belongs at a typed boundary instead.
Red Flags
JSON.parse(...) as FooNumber(value) || 0input.trim().toLowerCase()with no product reason- repeated
foo ?? defaultacross business logic - generic
sanitize*ornormalize*helpers with vague scope - large feature-local helpers that should be split into small composable functions
- new feature-local string, number, URL, array, or dictionary helpers that should live in a shared helper file
- duplicated boundary transforms across multiple files
- domain types carrying both
nullandundefinedwithout a good reason - broad interfaces or classes passed through layers that only need one or two methods
- newly added or expanded code comments without an explicit user request
- stale existing comments left behind after the code they describe changes
Review Checklist
- Is raw JSON being validated with Zod instead of cast?
- Are functions small and composable?
- Is boundary parsing happening once and early?
- Are boundary DTOs or view shapes translated once into internal domain types?
- Is external payload variance handled at the boundary rather than leaking into internal logic?
- Is the code using
nullandundefineddeliberately instead of mixing both through domain code? - Is the code normalizing values without a clear boundary or product reason?
- If normalization is required, does the code preserve the original value and add a derived key?
- Is there already an existing helper, transform, or adapter in the repo that should be reused?
- For foundational string, number, URL, array, dictionary, dedupe, or filter logic, did the code search for and use a shared helper file instead of adding feature-local duplication?
- Should repeated invariants or boundary adapters be extracted instead of copied again?
- Did a
jscpdsmoke test or targetedrgsearch identify duplicate code worth refactoring? - Is
asbeing used where stronger checks or better types should exist instead? - Are domain invariants modeled with exact types like unions,
Map,Set, oras constvalues? - Does the code depend on the smallest capability surface it needs?
- Did the change avoid adding or expanding code comments unless the user explicitly requested them?
- Were stale existing comments removed instead of rewritten when comments were not requested?
- Is the code hiding broken assumptions behind weak defaults or silent fallbacks?
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/justbecauselabs/agent-skills/typescript-best-practices">View typescript-best-practices on skillZs</a>