skills/typescript-advanced-patterns/SKILL.md
Advanced TypeScript type system patterns for production codebases. [What: branded types for nominal typing, discriminated unions, template literal types, conditional types, the infer keyword, satisfies operator, const assertions, Zod schema inference, type-safe event emitters, exhaustive switch checking] [When: designing domain models, building type-safe APIs, creating reusable generic utilities, eliminating runtime bugs with compile-time guarantees, refactoring any-typed codebases] [Keywords: branded types, discriminated union, template literal types, conditional types, infer, satisfies, const assertion, Zod inference, exhaustive, mapped types, utility types, nominal typing, type narrowing, generic constraints] NOT for basic TypeScript syntax or React component typing (use a React-specific skill).
npx skillsauth add curiositech/windags-skills typescript-advanced-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Advanced type system patterns that eliminate runtime bugs by encoding constraints at compile time.
1. Are you mixing values of the same primitive type?
├─ YES: ID confusion (UserId vs OrderId) → Use Branded Types
├─ YES: Money confusion (USD vs EUR, dollars vs cents) → Use Branded Types with validation
└─ NO: Continue to #2
2. Do you have a value that can be one of N different shapes?
├─ YES: API responses (success/error/loading) → Use Discriminated Unions
├─ YES: State machine states → Use Discriminated Unions with exhaustive checking
└─ NO: Continue to #3
3. Are you parsing external data (APIs, user input)?
├─ YES: Unknown JSON shape → Use Zod schema + z.infer<typeof Schema>
├─ YES: Form validation → Use Zod with branded types for validated inputs
└─ NO: Continue to #4
4. Do you need types that compute based on other types?
├─ YES: Extract function parameters → Use conditional types with infer
├─ YES: Transform object shapes → Use mapped types with template literals
└─ NO: Continue to #5
5. Are you validating without losing specific type info?
├─ YES: Config objects with optional fields → Use satisfies operator
├─ YES: Const arrays that need narrow types → Use const assertions
└─ NO: Review if advanced patterns are needed
IF (primitive mixing bugs possible)
→ Start with branded types for domain IDs
→ Add Zod constructors for validation
IF (multiple related states)
→ Define discriminated union with 'kind'/'type'/'status' field
→ Add exhaustive switch with assertNever default
IF (external data + type safety needed)
→ Define Zod schema first
→ Export type as z.infer<typeof Schema>
→ Never manually write types for external data
IF (generic utilities needed)
→ Use conditional types with infer for extraction
→ Add constraints to prevent misuse
→ Test with expect-type for complex utilities
Symptom: Every string and number in codebase is branded
Detection: If you see Brand<string, 'FirstName'> and Brand<string, 'LastName'> that are never mixed up
Root Cause: Treating branding as general "make types stricter" instead of "prevent specific mixing bugs"
Fix: Only brand when there's actual confusion risk (IDs, money, different units)
Symptom: Zod schemas with 50+ fields, nested 5+ levels deep
Detection: Schema definitions longer than the components that use them
Root Cause: Trying to validate entire API response instead of just the fields you use
Fix: Parse only what you need - z.object({ id: z.string(), status: z.enum(['active', 'inactive']) }) instead of full user object
Symptom: Multiple as Type casts or as any to make TypeScript "stop complaining"
Detection: More than 2 type assertions in a single function
Root Cause: Fighting the type system instead of designing types that match data flow
Fix: Use type guards, Zod parsing, or unknown with proper narrowing
Symptom: Union types work in some places but not others, "Property does not exist" errors Detection: TypeScript can't narrow the union in switch statements Root Cause: Discriminant field missing or inconsistent across union members Fix: Every union member must have same discriminant field with literal type
Symptom: TypeScript allows invalid state combinations like { status: 'success', error: string }
Detection: Properties from wrong union branch are accessible
Root Cause: Discriminant field values overlap or missing literal types
Fix: Use non-overlapping literal types in discriminant field, validate each branch is exclusive
Starting Point: Legacy API client with any types
// Before: any everywhere, runtime errors common
async function fetchUser(id: string): Promise<any> {
const response = await fetch(`/api/users/${id}`);
return response.json(); // any
}
// Usage leads to runtime errors
const userData = await fetchUser('123');
console.log(userData.user.profile.name); // Works sometimes, crashes others
Step 1: Analyze actual API responses (what novice skips)
// Expert examines actual responses first:
// Success: { status: 'success', user: { id: string, name: string } }
// Not found: { status: 'error', code: 404, message: 'User not found' }
// Server error: { status: 'error', code: 500, message: 'Internal error' }
Step 2: Define discriminated union (novice would create separate types)
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; code: number; message: string };
type User = {
id: string;
name: string;
email: string;
};
Step 3: Create Zod schema (expert validates at runtime boundary)
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
const ApiResponseSchema = <T extends z.ZodType>(dataSchema: T) =>
z.discriminatedUnion('status', [
z.object({
status: z.literal('success'),
data: dataSchema,
}),
z.object({
status: z.literal('error'),
code: z.number(),
message: z.string(),
}),
]);
type ApiResponse<T> = z.infer<ReturnType<typeof ApiResponseSchema>>;
Step 4: Type-safe client (novice forgets error handling)
async function fetchUser(id: string): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
const rawData: unknown = await response.json();
// Parse with Zod - throws on invalid shape
return ApiResponseSchema(UserSchema).parse(rawData);
}
// Usage forces error handling
const userResult = await fetchUser('123');
switch (userResult.status) {
case 'success':
console.log(userResult.data.name); // TypeScript knows data exists
break;
case 'error':
console.error(`Error ${userResult.code}: ${userResult.message}`);
break;
}
Trade-offs expert considers (novice misses):
any types - use unknown at boundaries with proper narrowingas Type) used only for proven safe narrows, never as anyT extends object not T extends any)Don't use for:
string, number[], { name: string })react-component-patterns skill insteadapi-architect skill for OpenAPI generationgraphql-typescript skill for codegenDelegate to other skills when:
api-architect for contract-first designvitest-testing-patterns for expect-type testsreact-performance-optimizer for memo patternserror-handling-strategies for Result typestools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.