types-first-guards
Skill8 starsChanged 4 months agoMITView on GitHub
Use strict TypeScript patterns — discriminated unions for domain concepts, exhaustive checks, nominal typing where needed. Use when writing domain logic that deals with user IDs, order IDs, or other concepts that should not be interchangeable even if their runtime type is the same.
In this repository30 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- cavemanSkill
- fastify-oramaSkill
- grill-me-mateoSkill
- improve-codebase-archSkill
- production-observabilitySkill
- prototypingSkill
- tddSkill
- to-issuesSkill
- to-prdSkill
- triageSkill
- write-a-skillSkill
- zoom-outSkill
- code-reviewSkill
- entity-normalizationSkill
- fastify-plugin-shapeSkill
- node-native-testsSkill
- result-not-throwSkill
- setup-mateonunez-skillsSkill
- single-tool-per-jobSkill
- vertical-slicesSkill
- grill-with-docsSkill
- a11y-default-reviewSkill
- aitSkill
- colemak-pair-programmingSkill
- disk-sweepSkill
- feature-flowSkill
- fleet-watchSkill
- mateonunez-coSkill
- conventional-commits-scopedSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 33
No reflexive runtime checks.
- Distinctive: line 65
Do NOT do this for every string.
- Very specific: line 71
Discriminated unions without exhaustiveness checking: If you write a switch on __type without handling all cases, the type system should …
- Very specific: line 84
Mixing domain and primitive types: Never do const userId: string when you have a UserId type.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Types First Guards
"If the types don't catch the error, the types are lying."
Philosophy
I use TypeScript as a guardrail against the entire class of "oops, I passed the wrong ID" bugs. The type system should make certain mistakes impossible at compile time, not catch them at runtime.
The Pattern
Nominal domain types for anything that should never be confused at the type level, even if runtime representation is identical:
type UserId = string & { readonly __brand: 'UserId' };
const userId = (id: string): UserId => id as UserId;
type OrgId = string & { readonly __brand: 'OrgId' };
const orgId = (id: string): OrgId => id as OrgId;
// Now this type-checks:
const user = await fetchUser(userId('42'));
const org = await fetchOrg(orgId('42'));
// But this does NOT:
const badUser = await fetchUser(orgId('42')); // ✗ Type errorNo reflexive runtime checks. The type system prevents the mistake in the first place.
Discriminated Unions for Multi-State Data
For domain concepts that have distinct states or variants, use __type discriminator unions:
type Order =
| { __type: 'pending'; id: OrderId; items: OrderItem[] }
| { __type: 'confirmed'; id: OrderId; items: OrderItem[]; confirmedAt: Date }
| { __type: 'shipped'; id: OrderId; items: OrderItem[]; trackingNumber: string }
| { __type: 'cancelled'; id: OrderId; reason: string };
function handleOrder(order: Order) {
switch (order.__type) {
case 'pending': return processPayment(order);
case 'confirmed': return scheduleShipment(order);
case 'shipped': return notifyCustomer(order);
case 'cancelled': return refund(order);
}
// ✓ TypeScript enforces exhaustiveness
}The compiler forces you to handle every case. Refactoring adds a state? The compiler catches you.
When Nominal Types Help Most
- IDs that should never be mixed: UserId, OrgId, ProjectId, OrderId. Use this pattern aggressively.
- Concepts from your domain glossary (in CONTEXT.md) that have different meanings even if they're structurally the same string/number.
- Auth boundaries: SessionToken vs RefreshToken vs ApiKey — different tokens, different lifespans, different threat models.
Do NOT do this for every string. Use it for semantic differences, not syntactic ones.
Anti-Patterns
unknownin business logic. Unknown is "I don't know the type." If you don't know, you're missing domain understanding. Dig until you do.as anyto make TypeScript shut up. This is a sign the types don't reflect reality. Fix the types, not the code.- Discriminated unions without exhaustiveness checking. If you write a switch on
__typewithout handling all cases, the type system should reject it. Ensure you're usingneverin unreachable branches to force exhaustiveness.
function handleOrder(order: Order) {
switch (order.__type) {
case 'pending': return processPayment(order);
case 'confirmed': return scheduleShipment(order);
// ✗ Missing 'shipped' and 'cancelled' cases
default: return never; // Type error: can't assign Order to never
}
}- Mixing domain and primitive types. Never do
const userId: stringwhen you have a UserId type. Always use the nominal type. - Type assertions on untrusted data at boundaries.
JSON.parse(userInput) as Orderis a lie. Validate first, then type-assert.
Phases
- Identify domain concepts — what are the distinct semantic types in your domain? (from CONTEXT.md if it exists)
- Create nominal types or discriminated unions for each
- Enforce at boundaries — HTTP handlers, DB queries, external API calls. Validate untrusted data, then assert types.
- Use in business logic — pass these types through your service layer so mistakes are caught early
- Verify exhaustiveness — switch statements on discriminated unions should have type-checker enforcement via
neverin unreachable code
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

