Clean TypeScript: Complete Reference
Enforces all Clean Code principles from Robert C. Martin's Chapter 17, adapted for TypeScript.
Comments (C1-C5)
- C1: No metadata in comments (use Git)
- C2: Delete obsolete comments immediately
- C3: No redundant comments
- C4: Write comments well if you must
- C5: Never commit commented-out code
Environment (E1-E2)
- E1: One command to build (
npm install or pnpm install)
- E2: One command to test (
npm test or pnpm test)
Functions (F1-F4)
- F1: Maximum 3 arguments (use interfaces for more)
- F2: No output arguments (return values)
- F3: No flag arguments (split functions)
- F4: Delete dead functions
General (G1-G36)
- G1: One language per file
- G2: Implement expected behavior
- G3: Handle boundary conditions
- G4: Don't override safeties
- G5: DRY - no duplication
- G6: Consistent abstraction levels
- G7: Base classes don't know children
- G8: Minimize public interface
- G9: Delete dead code
- G10: Variables near usage
- G11: Be consistent
- G12: Remove clutter
- G13: No artificial coupling
- G14: No feature envy
- G15: No selector arguments
- G16: No obscured intent
- G17: Code where expected
- G18: Prefer instance methods
- G19: Use explanatory variables
- G20: Function names say what they do
- G21: Understand the algorithm
- G22: Make dependencies physical
- G23: Prefer polymorphism to if/else
- G24: Follow conventions (ESLint, Prettier)
- G25: Named constants, not magic numbers
- G26: Be precise
- G27: Structure over convention
- G28: Encapsulate conditionals
- G29: Avoid negative conditionals
- G30: Functions do one thing
- G31: Make temporal coupling explicit
- G32: Don't be arbitrary
- G33: Encapsulate boundary conditions
- G34: One abstraction level per function
- G35: Config at high levels
- G36: Law of Demeter (no train wrecks)
TypeScript-Specific (TS1-TS10)
These adapt the Java-specific rules (J1-J3) to TypeScript conventions:
- TS1: Use explicit types on public interfaces — TypeScript's equivalent of Java's static typing
- TS2: Use Enums or const objects, not magic constants — same principle as J3
- TS3: Prefer named exports over default exports — clearer imports and better refactoring
- TS4: Always add explicit types everywhere — never rely on type inference for variables, parameters, return types, or callbacks
- TS5: Return statement formatting — ONLY
return; (no value) inline, return value always with braces
- TS6: Interfaces in separate files — never mix interfaces with functions in the same file
- TS7: No underscore for unused parameters — use descriptive name or omit entirely
- TS8: Use
undefined instead of null — consistent absence representation
- TS9: Never use
any — use unknown, generics, or proper types
- TS10: No destructuring — access properties directly via dot notation
- TS11: No single-letter variables — use descriptive names in loops, map, filter, reduce
// TS5: Empty return (guard clause) — single line, no braces
// Bad
if (data === undefined) {
return;
}
// Good - ONLY when return has NO value
if (data === undefined) return;
// TS5: Return with value — ALWAYS use braces (NEVER inline!)
// Bad - returns with values should NEVER be inline
if (data === undefined) return false;
if (items.length === 0) return [];
if (foundIds.has(id)) return false;
// Good - returns with values ALWAYS get braces
if (data === undefined) {
return false;
}
if (items.length === 0) {
return [];
}
if (foundIds.has(id)) {
return false;
}
// TS6: Interfaces in separate files
// Bad - mixing interfaces with functions
// file: userService.ts
interface User {
id: string;
name: string;
}
const createUser = (name: string): User => { /* ... */ };
// Good - separate files
// file: types/user.ts
interface User {
id: string;
name: string;
}
// file: userService.ts
import { User } from './types/user';
const createUser = (name: string): User => { /* ... */ };
// TS7: No underscore for unused parameters
// Bad
const handleEvent: EventHandler = async (_, context): Promise<void> => {
await processContext(context);
};
// Good - use descriptive name even if unused
const handleEvent: EventHandler = async (event, context): Promise<void> => {
await processContext(context);
};
// TS8: Use undefined instead of null
// Bad
const findUser = (id: string): User | null => {
return users.get(id) ?? null;
};
// Good
const findUser = (id: string): User | undefined => {
return users.get(id);
};
// TS9: Never use any
// Bad
const processData = (data: any): any => { /* ... */ };
// Good - use unknown and narrow, or generics
const processData = <T>(data: T): T => { /* ... */ };
const parseInput = (data: unknown): ParsedData => {
if (!isValidInput(data)) {
throw new Error("Invalid input");
}
return data as ParsedData;
};
// TS10: No destructuring — use dot notation
// Bad
const { x, y, z } = vector;
const distance: number = Math.sqrt(x * x + y * y + z * z);
const { name, email, age } = user;
console.log(name, email, age);
// Good - access properties directly
const distance: number = Math.sqrt(
vector.x * vector.x + vector.y * vector.y + vector.z * vector.z
);
console.log(user.name, user.email, user.age);
// TS11: No single-letter variables — use descriptive names
// Bad
users.map((u) => u.name);
items.filter((i) => i.active);
numbers.reduce((a, b) => a + b, 0);
for (const x of events) { /* ... */ }
// Good
users.map((user: User): string => user.name);
items.filter((item: Item): boolean => item.active);
numbers.reduce((sum: number, value: number): number => sum + value, 0);
for (const event of events) { /* ... */ }
Names (N1-N7)
- N1: Choose descriptive names
- N2: Right abstraction level
- N3: Use standard nomenclature
- N4: Unambiguous names
- N5: Name length matches scope
- N6: No encodings
- N7: Names describe side effects
Tests (T1-T9)
- T1: Test everything that could break
- T2: Use coverage tools
- T3: Don't skip trivial tests
- T4: Ignored test = ambiguity question
- T5: Test boundary conditions
- T6: Exhaustively test near bugs
- T7: Look for patterns in failures
- T8: Check coverage when debugging
- T9: Tests must be fast (< 100ms each)
Quick Reference Table
| Category |
Rule |
One-Liner |
| Comments |
C1 |
No metadata (use Git) |
|
C3 |
No redundant comments |
|
C5 |
No commented-out code |
| Functions |
F1 |
Max 3 arguments |
|
F3 |
No flag arguments |
|
F4 |
Delete dead functions |
| General |
G5 |
DRY—no duplication |
|
G9 |
Delete dead code |
|
G16 |
No obscured intent |
|
G23 |
Polymorphism over if/else |
|
G25 |
Named constants, not magic numbers |
|
G30 |
Functions do one thing |
|
G36 |
Law of Demeter (one dot) |
| Names |
N1 |
Descriptive names |
|
N5 |
Name length matches scope |
| Tests |
T5 |
Test boundary conditions |
|
T9 |
Tests must be fast |
| TypeScript |
TS1 |
Explicit types on public interfaces |
|
TS4 |
Always add explicit types everywhere |
|
TS5 |
ONLY return; inline, return value with braces |
|
TS6 |
Interfaces in separate files |
|
TS7 |
No underscore for unused parameters |
|
TS8 |
Use undefined not null |
|
TS9 |
Never use any |
|
TS10 |
No destructuring, use dot notation |
|
TS11 |
No single-letter variables |
Anti-Patterns (Don't → Do)
| ❌ Don't |
✅ Do |
| Comment every line |
Delete obvious comments |
| Helper for one-liner |
Inline the code |
import * as x |
Explicit named imports |
Magic number 86400 |
const SECONDS_PER_DAY: number = 86400 |
process(data, true) |
processVerbose(data) |
| Deep nesting |
Guard clauses, early returns |
obj.a.b.c.value |
obj.getValue() |
| 100+ line function |
Split by responsibility |
const x = 5 |
const x: number = 5 |
(val) => val > 0 |
(val: number): boolean => val > 0 |
if (x) { return; } |
if (x) return; |
if (x) return false; |
if (x) { return false; } |
| Interface + function in one file |
Separate types/ files for interfaces |
(_, ctx) => ... |
(event, ctx) => ... |
User | null |
User | undefined |
data: any |
data: unknown or generics |
const { x, y } = obj |
obj.x, obj.y |
.map((u) => ...) |
.map((user: User) => ...) |
.reduce((a, b) => ...) |
.reduce((sum, value) => ...) |
AI Behavior
When reviewing code, identify violations by rule number (e.g., "G5 violation: duplicated logic").
When fixing or editing code, report what was fixed (e.g., "Fixed: extracted magic number to SECONDS_PER_DAY (G25)").
1---2name: typescript-clean-code3description: Use when writing, fixing, editing, reviewing, or refactoring any TypeScript code. Enforces Robert Martin's complete Clean Code catalog—naming, functions, comments, DRY, and boundary conditions.4---56# Clean TypeScript: Complete Reference78Enforces all Clean Code principles from Robert C. Martin's Chapter 17, adapted for TypeScript.910## Comments (C1-C5)11- C1: No metadata in comments (use Git)12- C2: Delete obsolete comments immediately13- C3: No redundant comments14- C4: Write comments well if you must15- C5: Never commit commented-out code1617## Environment (E1-E2)18- E1: One command to build (`npm install` or `pnpm install`)19- E2: One command to test (`npm test` or `pnpm test`)2021## Functions (F1-F4)22- F1: Maximum 3 arguments (use interfaces for more)23- F2: No output arguments (return values)24- F3: No flag arguments (split functions)25- F4: Delete dead functions2627## General (G1-G36)28- G1: One language per file29- G2: Implement expected behavior30- G3: Handle boundary conditions31- G4: Don't override safeties32- G5: DRY - no duplication33- G6: Consistent abstraction levels34- G7: Base classes don't know children35- G8: Minimize public interface36- G9: Delete dead code37- G10: Variables near usage38- G11: Be consistent39- G12: Remove clutter40- G13: No artificial coupling41- G14: No feature envy42- G15: No selector arguments43- G16: No obscured intent44- G17: Code where expected45- G18: Prefer instance methods46- G19: Use explanatory variables47- G20: Function names say what they do48- G21: Understand the algorithm49- G22: Make dependencies physical50- G23: Prefer polymorphism to if/else51- G24: Follow conventions (ESLint, Prettier)52- G25: Named constants, not magic numbers53- G26: Be precise54- G27: Structure over convention55- G28: Encapsulate conditionals56- G29: Avoid negative conditionals57- G30: Functions do one thing58- G31: Make temporal coupling explicit59- G32: Don't be arbitrary60- G33: Encapsulate boundary conditions61- G34: One abstraction level per function62- G35: Config at high levels63- G36: Law of Demeter (no train wrecks)6465## TypeScript-Specific (TS1-TS10)66These adapt the Java-specific rules (J1-J3) to TypeScript conventions:67- TS1: Use explicit types on public interfaces — TypeScript's equivalent of Java's static typing68- TS2: Use Enums or const objects, not magic constants — same principle as J369- TS3: Prefer named exports over default exports — clearer imports and better refactoring70- TS4: Always add explicit types everywhere — never rely on type inference for variables, parameters, return types, or callbacks71- TS5: Return statement formatting — ONLY `return;` (no value) inline, `return value` always with braces72- TS6: Interfaces in separate files — never mix interfaces with functions in the same file73- TS7: No underscore for unused parameters — use descriptive name or omit entirely74- TS8: Use `undefined` instead of `null` — consistent absence representation75- TS9: Never use `any` — use `unknown`, generics, or proper types76- TS10: No destructuring — access properties directly via dot notation77- TS11: No single-letter variables — use descriptive names in loops, map, filter, reduce7879```typescript80// TS5: Empty return (guard clause) — single line, no braces81// Bad82if (data === undefined) {83 return;84}8586// Good - ONLY when return has NO value87if (data === undefined) return;8889// TS5: Return with value — ALWAYS use braces (NEVER inline!)90// Bad - returns with values should NEVER be inline91if (data === undefined) return false;92if (items.length === 0) return [];93if (foundIds.has(id)) return false;9495// Good - returns with values ALWAYS get braces96if (data === undefined) {97 return false;98}99if (items.length === 0) {100 return [];101}102if (foundIds.has(id)) {103 return false;104}105106// TS6: Interfaces in separate files107// Bad - mixing interfaces with functions108// file: userService.ts109interface User {110 id: string;111 name: string;112}113const createUser = (name: string): User => { /* ... */ };114115// Good - separate files116// file: types/user.ts117interface User {118 id: string;119 name: string;120}121// file: userService.ts122import { User } from './types/user';123const createUser = (name: string): User => { /* ... */ };124125// TS7: No underscore for unused parameters126// Bad127const handleEvent: EventHandler = async (_, context): Promise<void> => {128 await processContext(context);129};130131// Good - use descriptive name even if unused132const handleEvent: EventHandler = async (event, context): Promise<void> => {133 await processContext(context);134};135136// TS8: Use undefined instead of null137// Bad138const findUser = (id: string): User | null => {139 return users.get(id) ?? null;140};141142// Good143const findUser = (id: string): User | undefined => {144 return users.get(id);145};146147// TS9: Never use any148// Bad149const processData = (data: any): any => { /* ... */ };150151// Good - use unknown and narrow, or generics152const processData = <T>(data: T): T => { /* ... */ };153const parseInput = (data: unknown): ParsedData => {154 if (!isValidInput(data)) {155 throw new Error("Invalid input");156 }157 return data as ParsedData;158};159160// TS10: No destructuring — use dot notation161// Bad162const { x, y, z } = vector;163const distance: number = Math.sqrt(x * x + y * y + z * z);164165const { name, email, age } = user;166console.log(name, email, age);167168// Good - access properties directly169const distance: number = Math.sqrt(170 vector.x * vector.x + vector.y * vector.y + vector.z * vector.z171);172173console.log(user.name, user.email, user.age);174175// TS11: No single-letter variables — use descriptive names176// Bad177users.map((u) => u.name);178items.filter((i) => i.active);179numbers.reduce((a, b) => a + b, 0);180for (const x of events) { /* ... */ }181182// Good183users.map((user: User): string => user.name);184items.filter((item: Item): boolean => item.active);185numbers.reduce((sum: number, value: number): number => sum + value, 0);186for (const event of events) { /* ... */ }187```188189## Names (N1-N7)190- N1: Choose descriptive names191- N2: Right abstraction level192- N3: Use standard nomenclature193- N4: Unambiguous names194- N5: Name length matches scope195- N6: No encodings196- N7: Names describe side effects197198## Tests (T1-T9)199- T1: Test everything that could break200- T2: Use coverage tools201- T3: Don't skip trivial tests202- T4: Ignored test = ambiguity question203- T5: Test boundary conditions204- T6: Exhaustively test near bugs205- T7: Look for patterns in failures206- T8: Check coverage when debugging207- T9: Tests must be fast (< 100ms each)208209## Quick Reference Table210211| Category | Rule | One-Liner |212|----------|------|-----------|213| **Comments** | C1 | No metadata (use Git) |214| | C3 | No redundant comments |215| | C5 | No commented-out code |216| **Functions** | F1 | Max 3 arguments |217| | F3 | No flag arguments |218| | F4 | Delete dead functions |219| **General** | G5 | DRY—no duplication |220| | G9 | Delete dead code |221| | G16 | No obscured intent |222| | G23 | Polymorphism over if/else |223| | G25 | Named constants, not magic numbers |224| | G30 | Functions do one thing |225| | G36 | Law of Demeter (one dot) |226| **Names** | N1 | Descriptive names |227| | N5 | Name length matches scope |228| **Tests** | T5 | Test boundary conditions |229| | T9 | Tests must be fast |230| **TypeScript** | TS1 | Explicit types on public interfaces |231| | TS4 | Always add explicit types everywhere |232| | TS5 | ONLY `return;` inline, `return value` with braces |233| | TS6 | Interfaces in separate files |234| | TS7 | No underscore for unused parameters |235| | TS8 | Use `undefined` not `null` |236| | TS9 | Never use `any` |237| | TS10 | No destructuring, use dot notation |238| | TS11 | No single-letter variables |239240## Anti-Patterns (Don't → Do)241242| ❌ Don't | ✅ Do |243|----------|-------|244| Comment every line | Delete obvious comments |245| Helper for one-liner | Inline the code |246| `import * as x` | Explicit named imports |247| Magic number `86400` | `const SECONDS_PER_DAY: number = 86400` |248| `process(data, true)` | `processVerbose(data)` |249| Deep nesting | Guard clauses, early returns |250| `obj.a.b.c.value` | `obj.getValue()` |251| 100+ line function | Split by responsibility |252| `const x = 5` | `const x: number = 5` |253| `(val) => val > 0` | `(val: number): boolean => val > 0` |254| `if (x) { return; }` | `if (x) return;` |255| `if (x) return false;` | `if (x) { return false; }` |256| Interface + function in one file | Separate `types/` files for interfaces |257| `(_, ctx) => ...` | `(event, ctx) => ...` |258| `User \| null` | `User \| undefined` |259| `data: any` | `data: unknown` or generics |260| `const { x, y } = obj` | `obj.x`, `obj.y` |261| `.map((u) => ...)` | `.map((user: User) => ...)` |262| `.reduce((a, b) => ...)` | `.reduce((sum, value) => ...)` |263264## AI Behavior265266When reviewing code, identify violations by rule number (e.g., "G5 violation: duplicated logic").267When fixing or editing code, report what was fixed (e.g., "Fixed: extracted magic number to `SECONDS_PER_DAY` (G25)").