Agent skill
typescript-guide
TypeScript/JavaScript guardrails, patterns, and best practices for AI-assisted development. Use when working with TypeScript (.ts, .tsx) or JavaScript (.js, .jsx) files, package.json, or tsconfig.json. Provides strict mode conventions, async patterns, testing standards, and module system guidelines.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/typescript-guide-ar4mirez-samuel
Metadata
Additional technical details for this skill
- author
- samuel
- version
- 1.0
- category
- language
- language
- typescript
- extensions
- .ts,.tsx,.js,.jsx
SKILL.md
TypeScript/JavaScript Guide
Applies to: TypeScript 5+, Node.js 20+, ES2022+, React, Server-Side JS
Core Principles
- Strict TypeScript: Enable all strict flags; treat type errors as build failures
- Immutability by Default: Use
const,readonly,as const, and spread operators; mutate only when profiling demands it - Explicit Types at Boundaries: All function signatures, API responses, and public interfaces must have explicit type annotations; infer internally
- Functional Patterns: Prefer pure functions, map/filter/reduce, and composition over classes and mutation
- Zero
any: Useunknownfor truly unknown data, Zod/io-ts for runtime narrowing; everyanyrequires a code-review comment explaining why
Guardrails
TypeScript Configuration
- Enable
"strict": true(this activatesstrictNullChecks,noImplicitAny,strictFunctionTypes, etc.) - Enable
"noUncheckedIndexedAccess": true(arrays and records returnT | undefined) - Enable
"noImplicitReturns": trueand"noFallthroughCasesInSwitch": true - Set
"target": "ES2022","module": "ESNext","moduleResolution": "bundler" - Never suppress errors with
@ts-ignore; use@ts-expect-errorwith a justification comment
Code Style
constoverlet; never usevar- Nullish coalescing (
??) over logical OR (||) for defaults (avoids falsy-value bugs with0,"",false) - Optional chaining (
?.) over nested null checks - Template literals over string concatenation
- Destructuring at call sites:
const { id, name } = user - Barrel exports (
index.ts) only at package boundaries, not within internal modules (causes circular imports and tree-shaking failures) - Enums: prefer
as constobjects over TypeScriptenum(enums produce runtime code and have surprising behaviors with reverse mappings)
// Prefer this
const Status = {
Active: "active",
Inactive: "inactive",
} as const;
type Status = (typeof Status)[keyof typeof Status];
// Over this
enum Status {
Active = "active",
Inactive = "inactive",
}
Error Handling
- Every
catchblock must type-narrow the error before accessing properties - Never throw primitive values (strings, numbers); always throw
Errorsubclasses - Use the
causeoption for error chaining:new AppError("msg", { cause: original }) - Never swallow errors with empty
catchblocks - Async functions must have error handling at the call site or a global boundary handler
Async/Await
- Always
awaitor return promises; never create fire-and-forget promises without explicitvoidannotation - Use
Promise.allfor independent concurrent operations, not sequentialawaitin a loop - Set timeouts on all external calls (fetch, database, third-party APIs) using
AbortController - Use
Promise.allSettledwhen partial failure is acceptable - Never mix
.then()chains withawaitin the same function
// Bad: sequential when order does not matter
const users = await fetchUsers();
const posts = await fetchPosts();
// Good: concurrent
const [users, posts] = await Promise.all([fetchUsers(), fetchPosts()]);
// Good: timeout with AbortController
async function fetchWithTimeout(url: string, ms: number): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), ms);
try {
return await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}
}
Module System
- Use ES modules (
import/export) exclusively; norequire()in TypeScript - One export per concept: prefer named exports over default exports (improves refactoring, grep-ability)
- Side-effect imports (
import "./setup") must be documented with a comment explaining why - Re-export from
index.tsonly at package/feature boundaries - Keep import order: stdlib/node builtins, external packages, internal modules, relative paths (enforce with ESLint
import/order)
Project Structure
myproject/
├── src/
│ ├── index.ts # Application entry point
│ ├── config/ # Environment, feature flags
│ │ └── env.ts # Validated env vars (Zod schema)
│ ├── domain/ # Business logic (no I/O)
│ │ ├── user.ts
│ │ └── order.ts
│ ├── services/ # Application services (orchestrate domain + I/O)
│ ├── repositories/ # Data access (database, external APIs)
│ ├── routes/ # HTTP handlers / controllers
│ ├── middleware/ # Express/Koa/Hono middleware
│ ├── utils/ # Pure utility functions
│ └── types/ # Shared type definitions
├── tests/
│ ├── unit/ # Mirror src/ structure
│ ├── integration/ # API and database tests
│ └── fixtures/ # Shared test data
├── tsconfig.json
├── package.json
├── .eslintrc.cjs
├── .prettierrc
└── vitest.config.ts
- Domain logic in
domain/must have zero I/O dependencies (pure functions, easy to test) - No business logic in route handlers; delegate to services
- Shared types in
types/; co-locate component-specific types with their module
Error Handling Patterns
Custom Application Errors
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500,
options?: ErrorOptions
) {
super(message, options);
this.name = "AppError";
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`, "NOT_FOUND", 404);
this.name = "NotFoundError";
}
}
Result Pattern (for Expected Failures)
type Result<T, E = AppError> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseAge(input: string): Result<number> {
const age = Number(input);
if (Number.isNaN(age) || age < 0 || age > 150) {
return { ok: false, error: new AppError("Invalid age", "VALIDATION") };
}
return { ok: true, value: age };
}
// Usage: caller must check before accessing value
const result = parseAge(input);
if (!result.ok) {
return res.status(400).json({ error: result.error.message });
}
console.log(result.value); // Type-safe: number
Typed Catch Blocks
try {
await externalService.call();
} catch (error: unknown) {
if (error instanceof AppError) {
logger.warn(error.message, { code: error.code });
return res.status(error.statusCode).json({ error: error.message });
}
// Unknown error: wrap and re-throw
throw new AppError("Unexpected failure", "INTERNAL", 500, { cause: error });
}
Testing
Standards
- Test runner: Vitest (preferred) or Jest
- Test files: co-located as
*.test.tsor undertests/mirroringsrc/ - Naming:
describe("ModuleName")withit("should [expected behavior] when [condition]") - Use
beforeEachfor setup; avoidbeforeAllfor mutable state - Coverage target: >80% for business logic, >60% overall
- No
anyin test files; test helpers must be typed - Mock at boundaries (HTTP, database, file system), not internal functions
Table-Driven Tests
import { describe, it, expect } from "vitest";
import { validateEmail } from "./validate";
describe("validateEmail", () => {
const cases = [
{ input: "[email protected]", expected: true, desc: "valid email" },
{ input: "no-at-sign", expected: false, desc: "missing @ symbol" },
{ input: "", expected: false, desc: "empty string" },
{ input: "[email protected]", expected: true, desc: "minimal valid email" },
] as const;
it.each(cases)("returns $expected for $desc", ({ input, expected }) => {
expect(validateEmail(input)).toBe(expected);
});
});
Mocking External Dependencies
import { describe, it, expect, vi, beforeEach } from "vitest";
import { UserService } from "./user.service";
import type { UserRepository } from "./user.repository";
describe("UserService", () => {
let service: UserService;
let repo: UserRepository;
beforeEach(() => {
repo = {
findById: vi.fn(),
save: vi.fn(),
} as unknown as UserRepository;
service = new UserService(repo);
});
it("should throw NotFoundError when user does not exist", async () => {
vi.mocked(repo.findById).mockResolvedValue(null);
await expect(service.getUser("abc")).rejects.toThrow("not found");
expect(repo.findById).toHaveBeenCalledWith("abc");
});
});
Tooling
Essential Commands
tsc --noEmit # Type check (no output)
eslint . --ext .ts,.tsx # Lint
prettier --check . # Format check
prettier --write . # Format fix
vitest # Run tests (watch mode)
vitest run # Run tests (CI mode)
vitest run --coverage # With coverage
ESLint Configuration (Flat Config)
// eslint.config.mjs
import tseslint from "typescript-eslint";
import importPlugin from "eslint-plugin-import";
export default tseslint.config(
...tseslint.configs.strictTypeChecked,
{
rules: {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": ["warn", {
allowExpressions: true,
}],
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/strict-boolean-expressions": "error",
"import/order": ["error", {
groups: ["builtin", "external", "internal", "parent", "sibling"],
"newlines-between": "always",
}],
},
}
);
Strict tsconfig.json
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"skipLibCheck": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"exactOptionalPropertyTypes": true,
"noPropertyAccessFromIndexSignature": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
}
}
Input Validation (Zod)
import { z } from "zod";
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150),
role: z.enum(["admin", "user", "viewer"]),
});
type CreateUserInput = z.infer<typeof CreateUserSchema>;
// At API boundary
function handleCreateUser(rawBody: unknown): CreateUserInput {
return CreateUserSchema.parse(rawBody); // throws ZodError on failure
}
Advanced Topics
For detailed patterns and examples, see:
- references/patterns.md -- Advanced type patterns, Zod validation, async orchestration, module organization, React + TypeScript idioms
- references/pitfalls.md -- Common TypeScript footguns, type narrowing mistakes, async traps
- references/security.md -- XSS prevention, CSRF, content security policy, dependency auditing
External References
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?