Effective TypeScript Skill
Apply the 62 items from Dan Vanderkam's "Effective TypeScript" to review existing code and write new TypeScript. This skill operates in two modes: Review Mode (analyze code for violations) and Write Mode (produce idiomatic, well-typed TypeScript from scratch).
Reference Files
This skill includes categorized reference files covering all 62 items:
ref-01-getting-to-know-ts.md — Items 1-5: TS/JS relationship, compiler options, code generation, structural typing, any
ref-02-type-system.md — Items 6-18: editor, sets, type vs value space, declarations vs assertions, object wrappers, excess property checking, generics, readonly, mapped types
ref-03-type-inference.md — Items 19-27: inferable types, widening, narrowing, objects at once, aliases, async/await, context, functional constructs
ref-04-type-design.md — Items 28-37: valid states, Postel's Law, documentation, null perimeter, unions of interfaces, string types, branded types
ref-05-working-with-any.md — Items 38-44: narrowest scope, precise any variants, unsafe assertions, evolving any, unknown, monkey patching, type coverage
ref-06-type-declarations.md — Items 45-52: devDependencies, three versions, export types, TSDoc, this in callbacks, conditional types, mirror types, testing types
ref-07-writing-running-code.md — Items 53-57: ECMAScript features, iterating objects, DOM hierarchy, private, source maps
ref-08-migrating.md — Items 58-62: modern JS, @ts-check, allowJs, module-by-module, noImplicitAny
How to Use This Skill
Before responding, read the relevant reference files based on the code's topic. For a general review, read all files. For targeted work (e.g., type design), read the specific reference (e.g., ref-04-type-design.md).
Mode 1: Code Review
When the user asks you to review existing TypeScript code, follow this process:
Step 1: Read Relevant References
Determine which chapters apply to the code under review and read those reference files. If unsure, read all of them.
Step 2: Analyze the Code
Before listing issues, first ask: Is this code already applying Effective TypeScript principles? Look for positive signals:
- Tagged unions with a discriminant field (Item 28/32)
- Branded types for nominal typing (Item 37)
unknown for external data, narrowed before use (Item 42)
- Type assertions scoped inside well-typed wrapper functions (Item 40)
readonly on fields/parameters (Item 17)
async/await with typed return types (Item 25)
- TSDoc comments on public functions (Item 48)
Key rule — Item 40 interaction with Item 9: A type assertion (as T) inside a function that has a fully-typed signature is NOT a violation of Item 9. Item 40 explicitly endorses hiding unsafe assertions inside well-typed wrappers. Only flag as when it appears at a call-site or as an escape hatch on a public-facing value.
For each relevant item from the book, check whether the code follows or violates the guideline. Focus on:
- TypeScript Fundamentals (Items 1-5): Is
strict mode enabled? Is any used carelessly? Does structural typing cause surprises?
- Type System Usage (Items 6-18): Are type declarations preferred over assertions? Are object wrapper types avoided? Are
readonly and mapped types used appropriately?
- Type Inference (Items 19-27): Is inference relied upon where possible? Are
async/await used over callbacks? Are aliases consistent?
- Type Design (Items 28-37): Do types represent only valid states? Are string types replaced with literal unions? Are null values pushed to the perimeter?
- Working with any (Items 38-44): Is
any scoped as narrowly as possible? Is unknown used for truly unknown values? Are unsafe assertions hidden in well-typed wrappers?
- Type Declarations (Items 45-52): Are
@types in devDependencies? Are public API types exported? Is TSDoc used for comments?
- Code Execution (Items 53-57): Are ECMAScript features preferred over TypeScript-only equivalents? Is object iteration done safely?
- Migration (Items 58-62): Is modern JavaScript used as a baseline? Is migration done module-by-module?
Step 3: Calibrate Your Response
If the code is already well-typed:
- Open with acknowledgment of what is correct and which Items are applied
- Only note genuine issues; do not manufacture problems
- Any remaining observations must be clearly labeled as "optional polish" or "minor suggestion"
- Do NOT escalate a narrowly scoped assertion inside a well-typed function to Critical/Important
If the code has real issues:
For each issue found, report:
- Item number and name (e.g., "Item 9: Prefer Type Declarations to Type Assertions")
- Location in the code
- What's wrong (the anti-pattern)
- How to fix it (the TypeScript-idiomatic way)
- Priority: Critical (bugs/correctness), Important (maintainability), Suggestion (style)
Step 4: Provide Fixed Code (only when needed)
If there are real issues, offer a corrected version with comments explaining each change. If the code is already correct, you may offer a brief "what's great here" summary instead of a rewrite.
Mode 2: Writing New Code
When the user asks you to write new TypeScript code, apply these core practices:
Always Apply These Core Practices
Enable strict mode (Item 2). Never write TypeScript without "strict": true in tsconfig.json.
Prefer type declarations over assertions (Item 9). Use const x: MyType = value not const x = value as MyType.
Avoid object wrapper types (Item 10). Use string, number, boolean — never String, Number, Boolean.
Use types that represent only valid states (Item 28). Eliminate impossible states at the type level with tagged unions.
Push null to the perimeter (Item 31). Don't scatter T | null throughout — handle nullability at boundaries.
Prefer unions of interfaces to interfaces of unions (Item 32). Model tagged unions instead of interfaces with optional fields that have implicit relationships.
Replace plain string types with string literal unions (Item 33). type Direction = 'north' | 'south' | 'east' | 'west' not string.
Generate types from APIs and specs, not data (Item 35). Use quicktype or OpenAPI code generation — don't hand-write types for external data.
Use unknown instead of any for values with unknown type (Item 42). unknown forces callers to narrow before use.
Scope any as narrowly as possible (Item 38). Apply it to a single value, never a whole object or module.
Use readonly to prevent mutation bugs (Item 17). Prefer readonly on function parameters accepting arrays, and on class fields that should not be reassigned.
Use async/await over raw Promises and callbacks (Item 25). It produces cleaner inferred types and clearer code.
Use type aliases to avoid repeating yourself (Item 14). DRY applies to types too — extract shared structure with Pick, Omit, mapped types.
Export all types that appear in public APIs (Item 47). Don't force users to reconstruct types with ReturnType<> or Parameters<>.
Use TSDoc for API comments (Item 48). /** */ comments appear in editor tooltips; @param, @returns, @deprecated are recognized by tooling.
Type Structure Template
// Prefer interfaces for object shapes (extendable); type aliases for unions/intersections
interface User {
readonly id: UserId; // Item 17: readonly on fields that shouldn't change
name: string;
email: string;
}
// Branded type for nominal typing (Item 37)
type UserId = string & { readonly __brand: 'UserId' };
// Tagged union — only valid states representable (Item 28, 32)
type RequestState<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string };
// unknown, not any, for values from external sources (Item 42)
function parseResponse(json: string): unknown {
return JSON.parse(json);
}
// async/await over callbacks (Item 25)
async function fetchUser(id: UserId): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as User; // narrowly scoped assertion inside well-typed function (Item 40)
}
any Guidelines
- If
any is unavoidable, apply it to the smallest possible scope (Item 38)
- Prefer
unknown for values received from external sources (Item 42)
- Hide unsafe assertions inside well-typed wrapper functions (Item 40)
- Track type coverage with
type-coverage CLI to prevent regressions (Item 44)
Priority of Items by Impact
Critical (Correctness & Bugs)
- Item 2: Enable
strict mode — noImplicitAny and strictNullChecks prevent whole classes of bugs
- Item 9: Prefer declarations to assertions — but see Item 40: assertions inside well-typed wrappers are fine
- Item 28: Types that always represent valid states — impossible states cause runtime errors
- Item 31: Push null to the perimeter — scattered nullability causes null dereferences
- Item 42: Use
unknown instead of any — any silently disables type checking
Item 40 exception: raw as SomeType inside a function with a fully-typed signature is explicitly endorsed by Item 40. It is acceptable and should NOT be flagged as a Critical or Important violation. At most, note it as a minor optional polish item (suggest a runtime validator like zod as a complement).
Important (Maintainability)
- Item 13: Know the differences between
type and interface
- Item 14: Use type operations and generics to avoid repetition
- Item 17: Use
readonly to prevent mutation bugs
- Item 25: Use
async/await over callbacks
- Item 32: Prefer unions of interfaces to interfaces of unions
- Item 33: Prefer string literal unions over plain
string
- Item 47: Export all types that appear in public APIs
- Item 48: Use TSDoc for API comments
Suggestions (Polish & Optimization)
- Item 19: Omit inferable types to reduce clutter
- Item 35: Generate types from APIs and specs
- Item 37: Consider brands for nominal typing
- Item 44: Track type coverage
- Item 53: Prefer ECMAScript features over TypeScript-only equivalents
Common Anti-Patterns to Flag
any overuse (Items 38, 42)
- Return type
Promise<any> — use Promise<unknown> or a concrete typed interface
as any cast at call-site or on a public-facing value — narrowest-scope rule violated
- Interface fields typed as
any (e.g., result?: any) — use unknown or a typed union
Interface of unions (Items 28, 32)
- An interface with
boolean + optional fields that only make sense together: { isLoading: boolean; data?: T; error?: string } is an interface-of-unions anti-pattern
- Invalid combinations are representable (e.g.,
{ isLoading: true, data: [...] } or { isLoading: false, data: undefined })
- Fix: replace with a tagged union using a
status discriminant field
- Non-null assertions (
!) inside render/display logic are a strong symptom of this pattern
Plain string parameters (Item 33)
- Function parameters typed as
string when only a small, known set of values is valid
- Fix:
type Theme = 'light' | 'dark' or similar literal union
Type assertions at call-sites (Item 9)
value as SomeType appearing in application code (not inside a well-typed wrapper function)
- Fix: use a type declaration
const x: SomeType = value or a validator function that returns the typed value
Missing strict mode (Item 2)
- TypeScript files without
"strict": true in tsconfig — whole classes of bugs (null dereferences, implicit any) go undetected
Callbacks over async/await (Item 25)
.then() chains instead of async/await — harder to read, worse type inference
Strengths to Recognize in Good Code
When reviewing code that applies these patterns correctly, explicitly acknowledge:
- Branded types (Item 37):
type FooId = string & { readonly __brand: 'FooId' } — prevents mixing up identifiers of different domain entities
- Tagged/discriminated unions (Item 28, 32):
type Result = { status: 'success'; data: T } | { status: 'error'; message: string } — only valid states representable
unknown for external data (Item 42): const raw: unknown = await response.json() — forces explicit narrowing before use
- Assertion inside well-typed wrapper (Item 40):
as T inside a function with a fully-typed signature — safe encapsulation of unsafe boundary
readonly fields (Item 17): readonly id: UserId on domain entity interfaces — communicates immutability intent
async/await (Item 25): cleaner than .then() chains, better type inference
- TSDoc on public functions (Item 48):
/** @param ... @returns ... */ — shows up in editor tooltips
- String literal unions (Item 33):
type Status = 'pending' | 'processing' | 'shipped' instead of string
1---2name: effective-typescript3description: Review existing TypeScript code and write new TypeScript following the 62 items from "Effective TypeScript" by Dan Vanderkam. Use when writing TypeScript, reviewing TypeScript code, working with type design, avoiding any, managing type declarations, or migrating JavaScript to TypeScript. Trigger on: "TypeScript best practices", "type safety", "any", "type assertions", "type design", "strict mode", "TypeScript review", "migrate to TypeScript".4license: MIT5---67# Effective TypeScript Skill89Apply the 62 items from Dan Vanderkam's "Effective TypeScript" to review existing code and write new TypeScript. This skill operates in two modes: **Review Mode** (analyze code for violations) and **Write Mode** (produce idiomatic, well-typed TypeScript from scratch).1011## Reference Files1213This skill includes categorized reference files covering all 62 items:1415- `ref-01-getting-to-know-ts.md` — Items 1-5: TS/JS relationship, compiler options, code generation, structural typing, any16- `ref-02-type-system.md` — Items 6-18: editor, sets, type vs value space, declarations vs assertions, object wrappers, excess property checking, generics, readonly, mapped types17- `ref-03-type-inference.md` — Items 19-27: inferable types, widening, narrowing, objects at once, aliases, async/await, context, functional constructs18- `ref-04-type-design.md` — Items 28-37: valid states, Postel's Law, documentation, null perimeter, unions of interfaces, string types, branded types19- `ref-05-working-with-any.md` — Items 38-44: narrowest scope, precise any variants, unsafe assertions, evolving any, unknown, monkey patching, type coverage20- `ref-06-type-declarations.md` — Items 45-52: devDependencies, three versions, export types, TSDoc, this in callbacks, conditional types, mirror types, testing types21- `ref-07-writing-running-code.md` — Items 53-57: ECMAScript features, iterating objects, DOM hierarchy, private, source maps22- `ref-08-migrating.md` — Items 58-62: modern JS, @ts-check, allowJs, module-by-module, noImplicitAny2324## How to Use This Skill2526**Before responding**, read the relevant reference files based on the code's topic. For a general review, read all files. For targeted work (e.g., type design), read the specific reference (e.g., `ref-04-type-design.md`).2728---2930## Mode 1: Code Review3132When the user asks you to **review** existing TypeScript code, follow this process:3334### Step 1: Read Relevant References35Determine which chapters apply to the code under review and read those reference files. If unsure, read all of them.3637### Step 2: Analyze the Code38Before listing issues, first ask: **Is this code already applying Effective TypeScript principles?** Look for positive signals:39- Tagged unions with a discriminant field (Item 28/32)40- Branded types for nominal typing (Item 37)41- `unknown` for external data, narrowed before use (Item 42)42- Type assertions scoped inside well-typed wrapper functions (Item 40)43- `readonly` on fields/parameters (Item 17)44- `async`/`await` with typed return types (Item 25)45- TSDoc comments on public functions (Item 48)4647**Key rule — Item 40 interaction with Item 9:** A type assertion (`as T`) inside a function that has a fully-typed signature is NOT a violation of Item 9. Item 40 explicitly endorses hiding unsafe assertions inside well-typed wrappers. Only flag `as` when it appears at a call-site or as an escape hatch on a public-facing value.4849For each relevant item from the book, check whether the code follows or violates the guideline. Focus on:50511. **TypeScript Fundamentals** (Items 1-5): Is `strict` mode enabled? Is `any` used carelessly? Does structural typing cause surprises?522. **Type System Usage** (Items 6-18): Are type declarations preferred over assertions? Are object wrapper types avoided? Are `readonly` and mapped types used appropriately?533. **Type Inference** (Items 19-27): Is inference relied upon where possible? Are `async`/`await` used over callbacks? Are aliases consistent?544. **Type Design** (Items 28-37): Do types represent only valid states? Are string types replaced with literal unions? Are null values pushed to the perimeter?555. **Working with any** (Items 38-44): Is `any` scoped as narrowly as possible? Is `unknown` used for truly unknown values? Are unsafe assertions hidden in well-typed wrappers?566. **Type Declarations** (Items 45-52): Are `@types` in devDependencies? Are public API types exported? Is TSDoc used for comments?577. **Code Execution** (Items 53-57): Are ECMAScript features preferred over TypeScript-only equivalents? Is object iteration done safely?588. **Migration** (Items 58-62): Is modern JavaScript used as a baseline? Is migration done module-by-module?5960### Step 3: Calibrate Your Response6162**If the code is already well-typed:**63- Open with acknowledgment of what is correct and which Items are applied64- Only note genuine issues; do not manufacture problems65- Any remaining observations must be clearly labeled as "optional polish" or "minor suggestion"66- Do NOT escalate a narrowly scoped assertion inside a well-typed function to Critical/Important6768**If the code has real issues:**69For each issue found, report:70- **Item number and name** (e.g., "Item 9: Prefer Type Declarations to Type Assertions")71- **Location** in the code72- **What's wrong** (the anti-pattern)73- **How to fix it** (the TypeScript-idiomatic way)74- **Priority**: Critical (bugs/correctness), Important (maintainability), Suggestion (style)7576### Step 4: Provide Fixed Code (only when needed)77If there are real issues, offer a corrected version with comments explaining each change. If the code is already correct, you may offer a brief "what's great here" summary instead of a rewrite.7879---8081## Mode 2: Writing New Code8283When the user asks you to **write** new TypeScript code, apply these core practices:8485<core_principles>8687### Always Apply These Core Practices88891. **Enable strict mode** (Item 2). Never write TypeScript without `"strict": true` in tsconfig.json.90912. **Prefer type declarations over assertions** (Item 9). Use `const x: MyType = value` not `const x = value as MyType`.92933. **Avoid object wrapper types** (Item 10). Use `string`, `number`, `boolean` — never `String`, `Number`, `Boolean`.94954. **Use types that represent only valid states** (Item 28). Eliminate impossible states at the type level with tagged unions.96975. **Push null to the perimeter** (Item 31). Don't scatter `T | null` throughout — handle nullability at boundaries.98996. **Prefer unions of interfaces to interfaces of unions** (Item 32). Model tagged unions instead of interfaces with optional fields that have implicit relationships.1001017. **Replace plain string types with string literal unions** (Item 33). `type Direction = 'north' | 'south' | 'east' | 'west'` not `string`.1021038. **Generate types from APIs and specs, not data** (Item 35). Use `quicktype` or OpenAPI code generation — don't hand-write types for external data.1041059. **Use `unknown` instead of `any` for values with unknown type** (Item 42). `unknown` forces callers to narrow before use.10610710. **Scope `any` as narrowly as possible** (Item 38). Apply it to a single value, never a whole object or module.10810911. **Use `readonly` to prevent mutation bugs** (Item 17). Prefer `readonly` on function parameters accepting arrays, and on class fields that should not be reassigned.11011112. **Use `async`/`await` over raw Promises and callbacks** (Item 25). It produces cleaner inferred types and clearer code.11211313. **Use type aliases to avoid repeating yourself** (Item 14). DRY applies to types too — extract shared structure with `Pick`, `Omit`, mapped types.11411514. **Export all types that appear in public APIs** (Item 47). Don't force users to reconstruct types with `ReturnType<>` or `Parameters<>`.11611715. **Use TSDoc for API comments** (Item 48). `/** */` comments appear in editor tooltips; `@param`, `@returns`, `@deprecated` are recognized by tooling.118119</core_principles>120121<examples>122123### Type Structure Template124125<example id="1" title="Core type patterns — branded types, tagged unions, unknown, async/await">126127```typescript128// Prefer interfaces for object shapes (extendable); type aliases for unions/intersections129interface User {130 readonly id: UserId; // Item 17: readonly on fields that shouldn't change131 name: string;132 email: string;133}134135// Branded type for nominal typing (Item 37)136type UserId = string & { readonly __brand: 'UserId' };137138// Tagged union — only valid states representable (Item 28, 32)139type RequestState<T> =140 | { status: 'loading' }141 | { status: 'success'; data: T }142 | { status: 'error'; message: string };143144// unknown, not any, for values from external sources (Item 42)145function parseResponse(json: string): unknown {146 return JSON.parse(json);147}148149// async/await over callbacks (Item 25)150async function fetchUser(id: UserId): Promise<User> {151 const response = await fetch(`/api/users/${id}`);152 if (!response.ok) throw new Error(`HTTP ${response.status}`);153 return response.json() as User; // narrowly scoped assertion inside well-typed function (Item 40)154}155```156157</example>158159</examples>160161<guidelines>162163### any Guidelines164- If `any` is unavoidable, apply it to the smallest possible scope (Item 38)165- Prefer `unknown` for values received from external sources (Item 42)166- Hide unsafe assertions inside well-typed wrapper functions (Item 40)167- Track type coverage with `type-coverage` CLI to prevent regressions (Item 44)168169</guidelines>170171---172173## Priority of Items by Impact174175<core_principles>176177### Critical (Correctness & Bugs)178- Item 2: Enable `strict` mode — `noImplicitAny` and `strictNullChecks` prevent whole classes of bugs179- Item 9: Prefer declarations to assertions — **but see Item 40**: assertions inside well-typed wrappers are fine180- Item 28: Types that always represent valid states — impossible states cause runtime errors181- Item 31: Push null to the perimeter — scattered nullability causes null dereferences182- Item 42: Use `unknown` instead of `any` — `any` silently disables type checking183184> **Item 40 exception:** `raw as SomeType` inside a function with a fully-typed signature is explicitly endorsed by Item 40. It is acceptable and should NOT be flagged as a Critical or Important violation. At most, note it as a minor optional polish item (suggest a runtime validator like zod as a complement).185186</core_principles>187188<guidelines>189190### Important (Maintainability)191- Item 13: Know the differences between `type` and `interface`192- Item 14: Use type operations and generics to avoid repetition193- Item 17: Use `readonly` to prevent mutation bugs194- Item 25: Use `async`/`await` over callbacks195- Item 32: Prefer unions of interfaces to interfaces of unions196- Item 33: Prefer string literal unions over plain `string`197- Item 47: Export all types that appear in public APIs198- Item 48: Use TSDoc for API comments199200### Suggestions (Polish & Optimization)201- Item 19: Omit inferable types to reduce clutter202- Item 35: Generate types from APIs and specs203- Item 37: Consider brands for nominal typing204- Item 44: Track type coverage205- Item 53: Prefer ECMAScript features over TypeScript-only equivalents206207</guidelines>208209<anti_patterns>210211## Common Anti-Patterns to Flag212213### any overuse (Items 38, 42)214- Return type `Promise<any>` — use `Promise<unknown>` or a concrete typed interface215- `as any` cast at call-site or on a public-facing value — narrowest-scope rule violated216- Interface fields typed as `any` (e.g., `result?: any`) — use `unknown` or a typed union217218### Interface of unions (Items 28, 32)219- An interface with `boolean` + optional fields that only make sense together: `{ isLoading: boolean; data?: T; error?: string }` is an interface-of-unions anti-pattern220 - Invalid combinations are representable (e.g., `{ isLoading: true, data: [...] }` or `{ isLoading: false, data: undefined }`)221 - Fix: replace with a tagged union using a `status` discriminant field222 - Non-null assertions (`!`) inside render/display logic are a strong symptom of this pattern223224### Plain string parameters (Item 33)225- Function parameters typed as `string` when only a small, known set of values is valid226 - Fix: `type Theme = 'light' | 'dark'` or similar literal union227228### Type assertions at call-sites (Item 9)229- `value as SomeType` appearing in application code (not inside a well-typed wrapper function)230 - Fix: use a type declaration `const x: SomeType = value` or a validator function that returns the typed value231232### Missing strict mode (Item 2)233- TypeScript files without `"strict": true` in tsconfig — whole classes of bugs (null dereferences, implicit any) go undetected234235### Callbacks over async/await (Item 25)236- `.then()` chains instead of `async`/`await` — harder to read, worse type inference237238</anti_patterns>239240<strengths_to_praise>241242## Strengths to Recognize in Good Code243244When reviewing code that applies these patterns correctly, explicitly acknowledge:245246- **Branded types** (Item 37): `type FooId = string & { readonly __brand: 'FooId' }` — prevents mixing up identifiers of different domain entities247- **Tagged/discriminated unions** (Item 28, 32): `type Result = { status: 'success'; data: T } | { status: 'error'; message: string }` — only valid states representable248- **`unknown` for external data** (Item 42): `const raw: unknown = await response.json()` — forces explicit narrowing before use249- **Assertion inside well-typed wrapper** (Item 40): `as T` inside a function with a fully-typed signature — safe encapsulation of unsafe boundary250- **`readonly` fields** (Item 17): `readonly id: UserId` on domain entity interfaces — communicates immutability intent251- **`async`/`await`** (Item 25): cleaner than `.then()` chains, better type inference252- **TSDoc on public functions** (Item 48): `/** @param ... @returns ... */` — shows up in editor tooltips253- **String literal unions** (Item 33): `type Status = 'pending' | 'processing' | 'shipped'` instead of `string`254255</strengths_to_praise>