Effect TS
tellahq/opensession
Write idiomatic Effect v4 TypeScript verified against the pinned effect@4.0.0-rc.112 source.
TypeScript strict typing: type narrowing, discriminated unions, Zod runtime validation, generic utility types, and Vitest testing.
$ npx skills add irahardianto/awesome-agv --skill typescript-idioms -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install irahardianto/awesome-agv typescript-idioms --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/irahardianto/awesome-agv.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/typescript-idioms .claude/skills/typescript-idioms && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "typescript-idioms" agent skill from https://github.com/irahardianto/awesome-agv/tree/main/.agents/skills/typescript-idioms into .claude/skills/typescript-idioms/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "typescript-idioms", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/irahardianto/awesome-agv/tree/main/.agents/skills/typescript-idiomsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add irahardianto/awesome-agv --skill typescript-idioms -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install irahardianto/awesome-agv typescript-idioms --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/irahardianto/awesome-agv.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.agents/skills/typescript-idioms .agents/skills/typescript-idioms && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "typescript-idioms" agent skill from https://github.com/irahardianto/awesome-agv/tree/main/.agents/skills/typescript-idioms into .agents/skills/typescript-idioms/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "typescript-idioms", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add irahardianto/awesome-agv --skill typescript-idioms -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install irahardianto/awesome-agv typescript-idioms --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/irahardianto/awesome-agv.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.agents/skills/typescript-idioms .cursor/skills/typescript-idioms && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "typescript-idioms" agent skill from https://github.com/irahardianto/awesome-agv/tree/main/.agents/skills/typescript-idioms into .cursor/skills/typescript-idioms/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "typescript-idioms", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/irahardianto/awesome-agv.git --path .agents/skills/typescript-idioms--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add irahardianto/awesome-agv --skill typescript-idioms -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install irahardianto/awesome-agv typescript-idioms --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/irahardianto/awesome-agv.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.agents/skills/typescript-idioms .gemini/skills/typescript-idioms && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "typescript-idioms" agent skill from https://github.com/irahardianto/awesome-agv/tree/main/.agents/skills/typescript-idioms into .gemini/skills/typescript-idioms/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "typescript-idioms", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install irahardianto/awesome-agv typescript-idiomsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add irahardianto/awesome-agv --skill typescript-idioms -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/irahardianto/awesome-agv.git skills-src && mkdir -p .github/skills && cp -r skills-src/.agents/skills/typescript-idioms .github/skills/typescript-idioms && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "typescript-idioms" agent skill from https://github.com/irahardianto/awesome-agv/tree/main/.agents/skills/typescript-idioms into .github/skills/typescript-idioms/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "typescript-idioms", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add irahardianto/awesome-agv --skill typescript-idioms -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install irahardianto/awesome-agv typescript-idioms --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/irahardianto/awesome-agv.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.agents/skills/typescript-idioms .opencode/skills/typescript-idioms && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "typescript-idioms" agent skill from https://github.com/irahardianto/awesome-agv/tree/main/.agents/skills/typescript-idioms into .opencode/skills/typescript-idioms/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "typescript-idioms", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
typescript-idiomsTypeScript strict typing: type narrowing, discriminated unions, Zod runtime validation, generic utility types, and Vitest testing.
Typescript Idioms is an agent skill from irahardianto/awesome-agv. TypeScript strict typing: type narrowing, discriminated unions, Zod runtime validation, generic utility types, and Vitest testing. Use when writing or refactoring TypeScript across frontend, backend, or full-stack projects.
Its SKILL.md is about 6.1k tokens, which your agent loads only when the skill is triggered. The skill folder holds 5 other files, including reference files (for example `references/project-structure.md`, `references/recommended-dependencies.md` and `references/ts-patterns-and-anti-patterns.md`).
It sits in Testing & QA, covering Forms and validation, Unit testing and Refactoring. It works with TypeScript, Zod, Vitest and Hono. The repository describes itself as: Comprehensive sets of standards and practices designed to elevate the capabilities of AI coding agents. The licence is MIT.
8 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 9e997ba. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
vitesttsceslintprettiercargonpmpnpmFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md. Its commands use npm and pnpm, which can reach the network depending on how they are called.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Typescript Idioms loads about 6.1k tokens when it runs, and up to ~17k if it reads all its reference files. Until then it costs about 60 tokens; SKILL.md has 1,451 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from irahardianto/awesome-agv at commit 9e997ba, republished under its MIT licence (© irahardianto). 1,451 words, ~6,124 tokens.
.claude/skills/typescript-idioms/SKILL.md (or your agent's skills folder). This skill also uses 4 other files; get the full folder from GitHub.TypeScript's type system is your documentation, your test, and your specification — all at once. Make the type system encode the invariants of your domain so that invalid states are unrepresentable. Lean into the compiler.
Scope: This file covers TypeScript-specific type system and language idioms. For framework-specific patterns, see the respective idiom skill (Vue, React, Angular, Next.js, Hono). For file layout, see
references/project-structure.md. For detailed safety, SAST patterns, and performance patterns, seereferences/ts-patterns-and-anti-patterns.md. For quality commands, see@.agents/rules/code-idioms-and-conventions.md. For logging library, see@.agents/skills/logging-implementation/SKILL.md.
Loading guards:
- Plain JavaScript (no
tsconfig.json): load@.agents/skills/javascript-idioms/SKILL.mdinstead — this skill assumes strict-mode TS.- Hono backend:
honohas no repo-level marker file, so it is not auto-detected. Ifhonoappears inpackage.jsondependencies, co-load@.agents/skills/hono-idioms/SKILL.mdalongside this skill.- Test-file naming diverges by framework: see
references/project-structure.md§ Test Organization for the reconciliation rule before creating test files.
Load these before writing code in the matching context — not after.
| Situation | Reference to Load |
|---|---|
| Starting a new project or setting up file layout | references/project-structure.md |
| Choosing packages, tsconfig template, or vitest config | references/recommended-dependencies.md |
| Writing code that handles user input, async operations, or I/O | references/ts-patterns-and-anti-patterns.md |
| Defining Zod schemas or validating API/env boundaries | references/zod-patterns.md |
tsx for development execution (replaces ts-node)All TypeScript projects MUST have strict mode enabled. If a project does not have these settings, fix tsconfig.json before proceeding.
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"forceConsistentCasingInFileNames": true
}
}Use unknown instead of any
// ❌ any disables type checking
function processPayload(payload: any) {
console.log(payload.id); // No error if id doesn't exist
}
// ✅ unknown forces narrowing
function processPayload(payload: unknown) {
if (typeof payload === 'object' && payload !== null && 'id' in payload) {
console.log(payload.id);
}
}Discriminated Unions for state machines
// ❌ Optional properties lead to impossible states
type State = {
status: 'loading' | 'success' | 'error';
data?: string;
error?: Error;
};
// ✅ Discriminated union makes impossible states unrepresentable
type State =
| { status: 'loading' }
| { status: 'success'; data: string }
| { status: 'error'; error: Error };Type narrowing with in and typeof
function handle(val: string | number) {
if (typeof val === 'string') {
// val is string
}
}Satisfies operator for checking without widening
type Colors = 'red' | 'green' | 'blue';
type RGB = [number, number, number];
// ❌ Record widens the specific keys
const palette1: Record<Colors, RGB> = { red: [255, 0, 0], green: [0, 255, 0], blue: [0, 0, 255] };
// ✅ satisfies keeps exact type
const palette2 = { red: [255, 0, 0], green: [0, 255, 0], blue: [0, 0, 255] } satisfies Record<Colors, RGB>;readonly everywhere
// ❌ Mutable arrays
function sum(numbers: number[]): number { ... }
// ✅ Readonly arrays
function sum(numbers: readonly number[]): number { ... }Opaque/Nominal typing for domain primitives
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };
function getUser(id: UserId): User { ... }
// ❌ Compile error — OrderId is not assignable to UserId
getUser(orderId);
// ✅ Explicit creation
const userId = 'u-123' as UserId;
getUser(userId);Template literal types for string patterns
type EventName = `on${Capitalize<string>}`;
type Route = `/${string}`;NoInfer<T> to prevent unwanted inference (TS 5.4+)
function createFSM<S extends string>(initial: S, transitions: Record<S, NoInfer<S>[]>) { ... }strictNullChecks (always).?.) over explicit checks// ❌ Verbose
const city = user && user.address && user.address.city;
// ✅ Concise
const city = user?.address?.city;??) over Logical OR (||)// ❌ Fails on 0 or ''
const count = input.count || 10;
// ✅ Only falls back on null/undefined
const count = input.count ?? 10;using declarations — TypeScript 5.2+)
Requires lib: ["es2022"] or higher in tsconfig.json. Available in all Node.js 24 LTS projects.// ✅ Automatic cleanup — resource disposed when scope exits
{
using file = await openFile('data.csv');
// file is automatically closed when block exits, even on throw
}Always throw Error instances, never primitives
// ❌ Loses stack trace, breaks instanceof checks
throw 'Something went wrong';
throw { message: 'fail' };
// ✅ Proper error with stack trace
throw new Error('Something went wrong');Custom error classes for domain errors
export class NotFoundError extends Error {
constructor(public readonly resource: string, public readonly id: string) {
super(`${resource} not found: ${id}`);
this.name = 'NotFoundError';
}
}Type-safe error narrowing
try {
await api.createUser(data);
} catch (err) {
// ❌ unsafe — err is unknown
console.log(err.message);
// ✅ narrowed
if (err instanceof NotFoundError) {
console.log(err.resource, err.id);
} else if (err instanceof Error) {
console.log(err.message);
}
}Exhaustive error handling with Result<T, E> discriminated union
Use this when you want to make errors part of the return type (no throw/catch required).
// Define once, reuse everywhere
type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E> = Ok<T> | Err<E>;
// Helper constructors eliminate boilerplate
const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
const err = <E>(error: E): Err<E> => ({ ok: false, error });
// Usage — no try/catch, caller is forced to handle the error case
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return err('Division by zero');
return ok(a / b);
}
const result = divide(10, 0);
if (!result.ok) {
console.error(result.error); // 'Division by zero'
} else {
console.log(result.value); // number
}Always use async/await over raw Promises.
Use Promise.all for parallel operations.
// ❌ Sequential
const users = await getUsers();
const posts = await getPosts();
// ✅ Parallel
const [users, posts] = await Promise.all([getUsers(), getPosts()]);Use Promise.allSettled when some can fail.
Avoid .then().catch() chains.
Abort long-running operations with AbortController
const controller = new AbortController();
const response = await fetch(url, { signal: controller.signal });
// Cancel if needed
controller.abort();Never use async callbacks in Array.forEach
// ❌ forEach ignores returned promises — operations run detached
items.forEach(async (item) => {
await process(item);
});
// ✅ Use for...of for sequential
for (const item of items) {
await process(item);
}
// ✅ Use Promise.all for concurrent
await Promise.all(items.map(item => process(item)));Handle timeouts with AbortSignal.timeout()
const response = await fetch(url, {
signal: AbortSignal.timeout(5000),
});TypeScript types do not exist at runtime. Any data crossing an I/O boundary must be validated.
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(2),
age: z.number().int().nonnegative(),
});
type User = z.infer<typeof UserSchema>;
function parseUser(data: unknown): User {
return UserSchema.parse(data);
}For advanced Zod patterns (transforms, discriminated unions, branded types, error formatting), see
references/zod-patterns.md.
Use Map/Set over plain objects for dynamic keys
// ❌ Plain objects as maps — prototype pollution risk, string-only keys
const cache: Record<string, User> = {};
// ✅ Map — any key type, no prototype chain, O(1) has/get/set
const cache = new Map<string, User>();Use structuredClone() for deep copies (not JSON round-trip)
// ❌ Lossy — drops undefined, functions, Date objects, BigInt
const copy = JSON.parse(JSON.stringify(original));
// ✅ Handles circular refs, Date, RegExp, Map, Set, ArrayBuffer
const copy = structuredClone(original);Prefer immutable array methods (ES2023+)
// ❌ Mutates original array
const sorted = arr.sort((a, b) => a - b);
const reversed = arr.reverse();
// ✅ Returns new array, original unchanged
const sorted = arr.toSorted((a, b) => a - b);
const reversed = arr.toReversed();
const withReplacement = arr.with(2, 'new');Use Object.groupBy() for grouping (ES2024)
Use Set for O(1) lookups instead of Array.includes in loops
Never use raw fetch spread throughout the codebase. Centralize it to handle tokens, retries, and errors.
Testability requirement: Always define an interface first. The concrete class implements it. This lets tests inject a
FakeHttpClientwithout network calls.
// ✅ Define interface first — enables test doubles (architectural rule: I/O isolation)
export interface HttpClient {
get<T>(path: string, schema: z.ZodType<T>): Promise<T>;
post<T>(path: string, body: unknown, schema: z.ZodType<T>): Promise<T>;
delete(path: string): Promise<void>;
}
// ✅ Production implementation
export class ApiClient implements HttpClient {
constructor(private readonly baseUrl: string) {}
async get<T>(path: string, schema: z.ZodType<T>): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw new ApiError(res.status, res.statusText);
const data: unknown = await res.json();
return schema.parse(data);
}
async post<T>(path: string, body: unknown, schema: z.ZodType<T>): Promise<T> {
const res = await fetch(`${this.baseUrl}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw new ApiError(res.status, res.statusText);
const data: unknown = await res.json();
return schema.parse(data);
}
async delete(path: string): Promise<void> {
const res = await fetch(`${this.baseUrl}${path}`, {
method: 'DELETE',
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) throw new ApiError(res.status, res.statusText);
}
}
// ✅ Test double — use in unit tests, no network required
export class FakeHttpClient implements HttpClient {
readonly calls: { method: string; path: string }[] = [];
private responses = new Map<string, unknown>();
enqueue(path: string, response: unknown): void {
this.responses.set(path, response);
}
async get<T>(path: string, schema: z.ZodType<T>): Promise<T> {
this.calls.push({ method: 'GET', path });
return schema.parse(this.responses.get(path));
}
async post<T>(path: string, body: unknown, schema: z.ZodType<T>): Promise<T> {
this.calls.push({ method: 'POST', path });
return schema.parse(this.responses.get(path));
}
async delete(path: string): Promise<void> {
this.calls.push({ method: 'DELETE', path });
}
}Parameter object over positional arguments
// ❌ Positional — error-prone, order-sensitive, boolean traps
function createUser(name: string, email: string, isAdmin: boolean, isActive: boolean) {}
// ✅ Named parameters — self-documenting, order-independent
interface CreateUserParams {
name: string;
email: string;
isAdmin: boolean;
isActive: boolean;
}
function createUser(params: CreateUserParams) {}Branded/Opaque types for domain primitives — see Type System Idioms §6. Never pass bare string or number for domain IDs.
Discriminated unions over inheritance — prefer union types with a type or kind discriminant over class hierarchies. Invalid states become compile errors.
Parse, don't validate — convert raw input into typed, validated domain objects at the boundary. Downstream code works with the typed form and never re-validates.
// ❌ Validate at every call site
function processOrder(orderId: string) {
if (!isValidUuid(orderId)) throw new Error('Invalid');
// ...
}
// ✅ Parse at boundary, use branded type everywhere else
const orderId = OrderIdSchema.parse(rawInput); // throws once at entry
processOrder(orderId); // OrderId is always validEarly returns to reduce nesting — use guard clauses instead of nested if/else.
// ❌ Deep nesting
function handle(req: Request) {
if (req.auth) {
if (req.auth.isValid) {
if (req.body) {
return process(req.body);
}
}
}
}
// ✅ Guard clauses
function handle(req: Request) {
if (!req.auth) return unauthorized();
if (!req.auth.isValid) return forbidden();
if (!req.body) return badRequest();
return process(req.body);
}Keep function complexity low (cyclomatic complexity < 10)
export default. Use named exports for better refactoring and intellisense.index.ts) sparingly. They can cause circular dependencies.// ✅ Ensures type is erased at runtime
import type { User } from './types';
import { parseUser } from './parser';NEVER suppress these rules — they signal structural problems that must be fixed:
| Rule | What It Signals | What To Do Instead |
|---|---|---|
@typescript-eslint/no-explicit-any | Type safety disabled | Use unknown and narrow |
@typescript-eslint/no-floating-promises | Unhandled async operation | Add await or void |
@typescript-eslint/no-unsafe-assignment | Unsafe type flow | Type the source properly |
@typescript-eslint/no-unnecessary-condition | Dead code or logic bug | Remove the condition |
complexity | Function too complex | Decompose into smaller functions |
Acceptable suppressions (with mandatory // SUPPRESS: comment):
| Rule | When Acceptable |
|---|---|
@typescript-eslint/no-non-null-assertion | After runtime validation proves non-null |
@typescript-eslint/ban-ts-comment | @ts-expect-error with explanation (never @ts-ignore) |
no-console | In CLI tools or development scripts |
Rule of thumb: If you're about to write // eslint-disable, stop and ask: "Am I suppressing a real design problem?" If yes, fix the design.
Use Vitest over Jest. It's faster, ESM-native, and requires zero config for TS.
AAA Pattern (Arrange, Act, Assert).
Test behavior, not implementation.
Test async errors by type, not message
Use vi.spyOn for interaction verification
Use satisfies for type-checked test fixtures
const mockUser = { id: '1', name: 'Test' } satisfies Partial<User>;Test coverage is non-negotiable for new code:
if/else, switch arm, error path) MUST be exercised@vitest/coverage-v8 to verify coverage locally before committing# Quick coverage check
vitest run --coverage
# Coverage with thresholds
vitest run --coverage --coverage.thresholds.lines=80Test double selection — choose the right tool:
| Approach | When to Use |
|---|---|
| Hand-written fake (implement interface) | Simple interface, few methods, need stateful behavior |
vi.fn() / vi.spyOn() | Verify call counts, argument matching |
msw (Mock Service Worker) | HTTP boundary mocking — intercepts at network level |
Parameterized it.each / test.each | Same logic, multiple input/output pairs |
Snapshot (expect().toMatchSnapshot()) | Large outputs — JSON responses, CLI output |
Prefer hand-written fakes for repository interfaces — they are simpler to debug and don't couple tests to implementation details. Use
vi.fn()when you genuinely need interaction verification.
tsc --noEmitis the TypeScript equivalent of Rust'scargo check— type-checks without producing output. It is the fastest possible feedback during TDD cycles.
| Phase | Command | Purpose |
|---|---|---|
| TDD / rapid iteration | tsc --noEmit | Type-check only, no emit — fastest loop |
| Pre-commit | eslint . | Static analysis — must pass with zero warnings |
| Pre-commit | prettier --write . | Formatting — non-negotiable, always run |
| Pre-commit | vitest run | Unit tests — must all pass |
| Coverage verification | vitest run --coverage | Verify before merging |
| Unused dep audit | knip | Run before releases |
Rules:
tsc build during TDD cycles — tsc --noEmit is sufficient and significantly faster.eslint . must pass with zero warnings before any commit. Warnings are treated as errors.prettier --write . is non-negotiable — all code must be formatted before committing.knip reports unused exports or dependencies, remove them before the release.Document all exported items:
@param + @returns + @throws// ❌ Undocumented exported function
export function parseUserToken(token: string): UserId { ... }
// ✅ Documented
/**
* Parses and validates a signed user token, returning the extracted UserId.
*
* @param token - JWT signed token from the Authorization header
* @returns Validated UserId branded type
* @throws {InvalidTokenError} if the token is expired, malformed, or signature invalid
*/
export function parseUserToken(token: string): UserId { ... }npm audit or pnpm audit in CIpackage.json with ^ for libraries (^3.0.0)package-lock.json or pnpm-lock.yaml) — for both apps and librariesknip before releasescrypto.randomUUID() over uuid packagestructuredClone() over lodash deep cloneArray.toSorted() over lodash sortObject.groupBy() over lodash groupByFor the full curated dependency list with versions, see
references/recommended-dependencies.md.
Never scatter process.env calls throughout the codebase
// ❌ Scattered, typo-prone, no validation
const port = process.env.PORT || '3000';
const dbUrl = process.env.DATABASE_URL;
// ✅ Centralized, validated at startup
import { z } from 'zod';
const EnvSchema = z.object({
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
export const env = EnvSchema.parse(process.env);Fail fast on missing required config at boot, not at first use
For type coercion traps, prototype pollution, scope bugs, security vulnerabilities, collection pitfalls, and performance invariants, see
references/ts-patterns-and-anti-patterns.md. Load it before writing any code handling user input, async operations, or I/O.For performance patterns, see
perf-optimizationskill.
© irahardianto, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 4 other files (references) in .agents/skills/typescript-idioms of irahardianto/awesome-agv.
Open the folder on GitHubat commit 9e997ba
Typescript Idioms next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Typescript Idioms this skillirahardianto/awesome-agv | 156 | — | ~6.1k | Automated safety check: Pass | MIT | |
| Effect TStellahq/opensession | 394 | — | ~3.7k | Automated safety check: Pass | MIT | |
| Effect TSmattiacerutti/supernova | 185 | — | ~2.8k | Automated safety check: Pass | MIT | |
| Effect TSpproenca/dot-skills | 215 | — | ~2k | Automated safety check: Pass | MIT | |
| Tabler Shared Lib Helperstabler/tabler | 42k | — | ~1.2k | Automated safety check: Pass | MIT | |
| Creating A Packagec15t/c15t | 1.9k | — | ~913 | Automated safety check: Pass | Apache-2.0 |
tellahq/opensession
Write idiomatic Effect v4 TypeScript verified against the pinned effect@4.0.0-rc.112 source.
mattiacerutti/supernova
Write idiomatic Effect v4 TypeScript following official best practices from effect-solutions and the Effect source.
pproenca/dot-skills
Effect-TS library usage in TypeScript — Effect.gen generators, Schema.Struct/Schema.Class definitions, Layer/Context.Tag/Service patterns, Effect.pipe pipelines, Data.TaggedError/Data.Class error…
tabler/tabler
Moves logic out of Astro frontmatter into tested helper modules under shared/lib, with rules for naming, typing, deterministic demo data and sibling test files.
c15t/c15t
Scaffold a new workspace package in the c15t monorepo. An agent skill from c15t/c15t.
jwynia/agent-skills
Develop AI agents, tools, and workflows with Mastra v1 Beta and Hono servers.
irahardianto/awesome-agv
Commits to one bold aesthetic direction, sets up a CSS token system for it, then builds the interface in Vue or plain HTML using those tokens.
irahardianto/awesome-agv
Profile-driven performance optimization protocol. An agent skill from irahardianto/awesome-agv.
irahardianto/awesome-agv
Coding conventions for Angular 19 and later: standalone components, signals, OnPush change detection, lazy routes and where RxJS still belongs.
irahardianto/awesome-agv
Rules for designing CI/CD pipelines in layers: universal lint, test and scan stages, container builds with SBOM attestation, and GitOps for orchestrated deployments.
irahardianto/awesome-agv
Hono lightweight web framework patterns: type-safe route handlers, middleware composition, Zod validation, and RPC clients for Cloudflare Workers, Node, or Bun.
irahardianto/awesome-agv
Mobile E2E testing patterns — Flutter integrationtest, Patrol, Maestro, golden testing, device matrix, and test data management.
Works with
Categories
TypeScript strict typing: type narrowing, discriminated unions, Zod runtime validation, generic utility types, and Vitest testing. Typescript Idioms is an agent skill from irahardianto/awesome-agv. TypeScript strict typing: type narrowing, discriminated unions, Zod runtime validation, generic utility types, and Vitest testing.
Typescript Idioms fits situations like: refactoring TypeScript across frontend; full-stack projects.
Run `npx skills add irahardianto/awesome-agv --skill typescript-idioms -a claude-code`. Or copy the skill folder (.agents/skills/typescript-idioms in irahardianto/awesome-agv) into .claude/skills/typescript-idioms in your project. Claude Code loads it when a task matches its description.
Run `npx skills add irahardianto/awesome-agv --skill typescript-idioms -a codex`. Or copy the skill folder (.agents/skills/typescript-idioms in irahardianto/awesome-agv) into .agents/skills/typescript-idioms in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add irahardianto/awesome-agv --skill typescript-idioms -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/typescript-idioms, .gemini/skills/typescript-idioms, .github/skills/typescript-idioms and .opencode/skills/typescript-idioms in your project.
Going by SKILL.md and its folder, Typescript Idioms needs the command-line tools its instructions call (vitest, tsc, eslint, prettier, cargo and npm). Our summary lists: Node.js.
SKILL.md contains no URLs. Its commands use npm, which can reach the network depending on how they are called. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Typescript Idioms is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 6.1k tokens (SKILL.md is roughly 24k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 11k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Typescript Idioms: Effect TS (tellahq/opensession, 394 stars), Effect TS (mattiacerutti/supernova, 185 stars), Effect TS (pproenca/dot-skills, 215 stars) and Tabler Shared Lib Helpers (tabler/tabler, 42k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
irahardianto (a GitHub user) maintains it in irahardianto/awesome-agv, which has 156 GitHub stars. The repository holds 34 skills in this directory. The repository was last updated on October 5, 2026.
Source: irahardianto/awesome-agv on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.