Writing quality full-stack TypeScript
SkillMonitoring & opsUse when writing or reviewing TypeScript/full-stack code. Encodes principles for type safety (branded types, discriminated unions, end-to-end types), real tests over mocks, OpenTelemetry observability, and picking the right abstractions instead of premature ones.
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 Writing quality full-stack TypeScript skill
What this skill tells your AI
The instructions your AI receives, as published by vvedantb/eva in .agents/skills/quality-code/SKILL.md and read by ahel’s review.
Apply these principles when writing or reviewing TypeScript code.
Make impossible states unrepresentable
Use the type system to make invalid states fail at compile time. Fewer reachable states = easier code to read and change.
Branded types
Brand primitives so they can't be mixed up. Validate once at the boundary; downstream code trusts the type.
type PhoneNumber = string & { __brand: "PhoneNumber" };
function parsePhone(input: string): PhoneNumber {
if (!/^\+?\d{10,15}$/.test(input)) throw new Error(`Invalid: ${input}`);
return input as PhoneNumber;
}
function sendSMS(to: PhoneNumber, body: string) {
/* input is trusted */
}
If the project already uses a library with native branded-type support (e.g. Effect), use their primitives instead of rolling your own.
Discriminated unions over flag bags
// Don't — invalid combos representable
type State = { loading: boolean; user?: User; error?: string };
// Do — only valid states exist
type State =
| { status: "loading" }
| { status: "success"; user: User }
| { status: "error"; error: string };
Let the types flow end-to-end
DB schema → server → client should share types without manual duplication. Use whatever end-to-end type tool the project already has (tRPC, oRPC, Elysia, TanStack Start). A users.email branded as Email should arrive on the client still branded.
Don't restate types you can derive. Reach for Pick, Omit, Parameters, ReturnType, Awaited, typeof etc. before writing a new interface. For function arguments, infer from the source instead of typing them by hand:
// Don't — duplicate shape, drifts when the row changes
type UserSummary = { id: string; email: Email };
function renderUser(u: UserSummary) {
/* ... */
}
// Do — derive from the source of truth
type User = Awaited<ReturnType<typeof db.query.users.findFirst>>;
function renderUser(u: Pick<User, "id" | "email">) {
/* ... */
}
Pass objects, not positional args
// Don't — swap two args, still compiles
sendEmail("Welcome!", "Hi there");
// Do — order-independent, self-documenting
sendEmail({ to: "alice@x.com", body: "Hi there" });
Skip on hot perf-critical paths; use elsewhere by default.
Standard Schema for shared validation
For libraries or code that doesn't want to pick a validator, accept StandardSchemaV1<unknown, T>.
Tests as real as possible
Don't mock things you can run. Spin up real services:
- LocalStack for AWS
- Miniflare for Cloudflare Workers
- Real Postgres/SQLite (e.g.
bun:sqlite), not a mock DB
Mock only third-party services that have no test environment.
OpenTelemetry, not print logging
When adding observability, instrument with OTel spans. The setup cost pays back the first time a user sends a request ID and you can answer instead of guess.
Signals
- GitHub stars
- 101
- Forks
- 7
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
quality-code- Source
- github.com/vvedantb/eva