TypeScript Development
SkillFiles & storageTypeScript best practices: strict mode, ESLint rules, file naming, imports, type safety, Zod validation, error handling, and testing with Vitest. Use when writing TypeScript code, reviewing for code quality, fixing lint errors, creating types/interfaces, writing tests, or adding validation.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the TypeScript Development skill
What this skill tells your AI
The instructions your AI receives, as published by erode-app/erode in .agents/skills/typescript-dev/SKILL.md and read by ahel’s review.
Strict Mode Essentials
Both projects use strict: true with these additional flags:
noUncheckedIndexedAccess— array/record indexing returnsT | undefined, must guard before usenoUnusedLocals/noUnusedParameters— prefix unused with_noImplicitReturns,noImplicitOverride,noFallthroughCasesInSwitch,noPropertyAccessFromIndexSignature(erode)
const items = ['a', 'b', 'c'];
const first = items[0]; // type: string | undefined
// ❌ first.toUpperCase() — might be undefined
// ✅ if (first !== undefined) { first.toUpperCase() }
ESLint Rules
Both projects use @typescript-eslint/strict-type-checked + stylisticTypeChecked.
eqeqeq— always use===/!==(erode allows== nullvia smart mode; forestry is strict)max-lines: 500(skipBlankLines, skipComments) — split files when approaching the limitno-unsafe-assignment,no-unsafe-member-access— never bypass withascastsrestrict-template-expressions— only strings/numbers in template literals (erode allows numbers)prefer-nullish-coalescing— use??over||for nullable values- Never suppress with
eslint-disable— fix the root cause
// ❌ Loose equality
if (value == '') { ... }
// ✅ Strict equality
if (value === '') { ... }
// ❌ Logical OR for nullable (falsy '' or 0 are lost)
const name = input || 'default';
// ✅ Nullish coalescing (only null/undefined trigger fallback)
const name = input ?? 'default';
File Naming & Imports
Enforced via eslint-plugin-check-file:
- kebab-case for all
.ts/.tsxfiles andsrc/folders .jsextensions in all local ESM imports (required by both Node16 and NodeNext resolution)
// ❌ Missing extension
import { validate } from './utils/validation';
// ✅ With .js extension
import { validate } from './utils/validation.js';
// ❌ File naming
src / utils / myHelper.ts;
src / UserService / index.ts;
// ✅ File naming
src / utils / my - helper.ts;
src / user - service / index.ts;
Type Safety
- Constrained types over raw
string/number— use literal unions, enums, or branded types for domain values with known sets unknownoverany— then narrow with type guards orinstanceofsatisfiesfor type-checked object literals that preserve literal types- Before creating types: search existing types, enums, and Zod schemas first — never duplicate
// ❌ function setStatus(status: string) — raw string for known values
// ✅ type Status = 'pending' | 'active' | 'archived';
// function setStatus(status: Status)
// ❌ function handle(input: any) { return input.name; }
// ✅ function handle(input: unknown) {
// if (typeof input === 'object' && input !== null && 'name' in input) { ... }
// }
Zod Validation
Schema-first pattern — define Zod schema, derive type with z.infer<>:
z.coercefor string-to-number conversion (CLI args, query params)- Wrap
schema.parse()in a helper that convertsZodErrorto project error type - Keep schemas in dedicated files (
src/schemas/orsrc/validation/), not inline in business logic
const ConfigSchema = z.object({
port: z.coerce.number().min(1).max(65535),
debug: z.boolean().default(false),
});
type Config = z.infer<typeof ConfigSchema>;
// Wrap parse in helper that converts ZodError to project error type
function validate<T>(schema: z.ZodType<T>, data: unknown): T {
try {
return schema.parse(data);
} catch (error) {
if (error instanceof z.ZodError) {
throw new AppError(`Validation failed: ${error.issues.map((i) => i.message).join(', ')}`);
}
throw error;
}
}
Error Handling
Base error class with code (enum), message, and context metadata:
- Set
this.namein constructor and callError.captureStackTrace static fromError(error: unknown)factory for safely wrapping unknown errors- Extend for domain-specific error types (config, API, validation)
class AppError extends Error {
constructor(
message: string,
public readonly code: ErrorCode,
public readonly context: Record<string, unknown> = {}
) {
super(message);
this.name = 'AppError';
Error.captureStackTrace(this, AppError);
}
static fromError(error: unknown, code = ErrorCode.UNKNOWN): AppError {
if (error instanceof AppError) return error;
if (error instanceof Error)
return new AppError(error.message, code, { originalError: error.name });
return new AppError(String(error), code);
}
}
Testing with Vitest
- Tests colocated near source in
__tests__/directories - Behavior-focused tests — test what the code does, not how
- Test file naming:
*.test.ts - Use
.jsextensions in test imports too
Avoid
anytype — useunknownand narroweslint-disablecomments — fix the root causeastype assertions to silence errors — use type guards orsatisfies- Duplicating existing types — search first
==comparisons — use===(== nullonly in smart-mode projects)- Files over 500 lines — split into focused modules
- Non-kebab-case file/folder names
- Missing
.jsextension in local imports - Inline Zod schemas in business logic — keep in schema files
- Untyped catch blocks —
errorisunknown, narrow before use - Raw
string/numberfor domain values with known constrained sets
Signals
- GitHub stars
- 37
- Forks
- 2
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
typescript-dev- Source
- github.com/erode-app/erode