| name | tandem-coder |
| description | Use when writing, fixing, editing, reviewing, refactoring, testing, or configuring TypeScript code in the Tandem project. Combines Tandem project rules, Boy Scout cleanup, Clean Code, TypeScript mastery, naming, functions, comments, general quality, and tests. |
| triggers | ["create","fix","refactor","improve","change","add","remove","update","merge","typescript","ts","type-safe","generics","react typescript","nestjs typescript","typescript migration","tsconfig","type guards","mapped types","conditional types","satisfies operator","zod validation","tests","rename","comments","clean code"] |
| when_to_use | Use this when you start coding in the Tandem project, whether it is a new feature, a bugfix, refactoring, test work, TypeScript type design, toolchain setup, or review.
Also trigger on: single-letter or cryptic identifiers (`d`, `x`, `proc`), Hungarian notation (`strName`, `arrUsers`, `nCount`), `I`-prefixed interfaces (`IUserRepository`), function names that hide side effects, ambiguous names like `rename(source, target)`, asks like "rename this" or "clearer name", functions or React components with 4+ parameters/props, boolean flag parameters like `isTest`, functions mutating parameters, unused exports, dead helper functions, asks like "too many props", "split this function", or "is this still used", commented-out code blocks, TODO/FIXME banners, author/ticket/date metadata in comments or TSDoc, TSDoc that no longer matches code, redundant comments, duplicated logic, magic numbers or hardcoded strings, long if/else chains that should be union types plus polymorphism, chained property access like `a.b.c.d`, functions juggling multiple responsibilities, clever one-liners, slow or flaky tests, skipped tests without clear reason, `test.only`, missing boundary cases, coverage gaps, edge cases, and "while you're at it" cleanup requests.
|
| version | 2.0.0 |
| category | tandem-typescript-engineering |
| tags | ["tandem","typescript","clean-code","tests","refactoring","type-safety","boy-scout"] |
Tandem Coder
Core principles for working with code in the Tandem project, expanded into the single canonical engineering skill. It merges the original tandem-coder, boy-scout, typescript-clean-code, mastering-typescript, clean-tests, clean-names, clean-general, clean-functions, and clean-comments skills.
Merged Source Skills
This file intentionally preserves and consolidates the behavior, trigger intent, rules, examples, and reference pointers from these source skills:
| Source skill | Original purpose preserved here |
|---|
tandem-coder | Core principles for working with code in the Tandem project |
boy-scout | Use when fixing, editing, changing, debugging, or working with any TypeScript code. Applies the Boy Scout Rule - always leave code cleaner than you found it |
typescript-clean-code | 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 |
mastering-typescript | Master enterprise-grade TypeScript development with type-safe patterns, modern tooling, framework integration, TypeScript 5.9+, generics, mapped types, conditional types, satisfies, Zod, React, NestJS, LangChain.js, Vite, pnpm, ESLint, and Vitest |
clean-tests | Use when writing, fixing, editing, or refactoring TypeScript tests. Enforces fast tests, boundary coverage, and one concept per test |
clean-names | Use when naming, renaming, or fixing variables, functions, classes, interfaces, or modules. Enforces descriptive names, appropriate length, and no encodings |
clean-general | Use when writing, fixing, editing, or reviewing TypeScript code quality. Enforces DRY, single responsibility, clear intent, no magic numbers, and proper abstractions |
clean-functions | Use when writing, fixing, editing, or refactoring TypeScript functions. Enforces max 3 arguments, single responsibility, and no flag parameters |
clean-comments | Use when writing, fixing, editing, or reviewing TypeScript comments and TSDoc. Enforces no metadata, no redundancy, and no commented-out code |
Metadata from mastering-typescript preserved for context: version 1.0.0, category programming-languages, author Richard Hightower, license MIT, tags typescript, type-safety, enterprise, react, nestjs, langchain, vite.
Operating Rule
When touching Tandem code, apply all of these layers together:
- Preserve Tandem's project architecture and file organization rules.
- Complete the requested task first.
- Apply the Boy Scout Rule: leave touched code a little cleaner than you found it.
- Enforce Clean Code rule numbers when reviewing or explaining quality issues.
- Keep TypeScript strict, explicit at boundaries, and runtime-validated where data crosses trust boundaries.
- Keep tests fast, focused, boundary-aware, and mirrored under
tests/.
Tandem Project Rules
Block Separation
Don't cram all constructs together; break the code into logical blocks. Leave a blank line between blocks for better readability.
function processData(data: number[]): number[] {
const result = [];
for (const item of data) {
if (item > 0) {
result.push(item * 2);
}
}
return result;
}
function processData(data: number[]): number[] {
const result = [];
for (const item of data) {
if (item > 0) {
result.push(item * 2);
}
}
return result;
}
File Organization
Interfaces and types are moved into separate files; final class files contain only the class without any extra code.
Don't use constants inline in arbitrary files; move them into thematic config files.
Write tests in the tests directory and organize them according to the structure of the files they test, so that it mirrors the main project structure.
Classes
1 class - 1 file. If a class grows to 150-200 lines, decide whether to split it into several classes.
This isn't an absolute size, but if it exceeds 250 lines, it's worth reconsidering.
Class Methods
Try to avoid large class methods. If a method grows to 70 lines, use private class methods; they help preserve code readability. It's allowed to make a class larger, ignoring the size-limit recommendations, in favor of readability.
Functions
Avoid standalone functions in files. If you need a function, find the relevant helper and use or create it.
The Boy Scout Rule
"Always leave the campground cleaner than you found it."
"Always check a module in cleaner than when you checked it out."
- Robert C. Martin, Clean Code
The Philosophy
You don't have to make every module perfect. You simply have to make it a little bit better than when you found it.
If we all followed this simple rule:
- Our systems would gradually get better as they evolved
- Teams would care for the system as a whole
- The relentless deterioration of software would end
When Working on Code
Every time you touch code, look for at least one small improvement.
Quick wins, do these immediately:
- Rename a poorly named variable
- Delete a redundant comment
- Remove dead code or unused imports
- Replace a magic number with a named constant
- Extract a deeply nested block into a well-named function
Deeper improvements, when time allows:
- Split a function that does multiple things
- Remove duplication (DRY)
- Add missing boundary checks
- Improve test coverage
The Rule in Practice
function proc(d: number[], x: number[], flag = false): number[] {
for (const i of d) {
if (i > 0) {
if (flag) {
x.push(i * 1.0825);
} else {
x.push(i);
}
}
}
return x;
}
const TAX_RATE = 0.0825;
function processPositiveValues(values: readonly number[], applyTax = false): number[] {
const rate = applyTax ? 1 + TAX_RATE : 1;
return values.filter((value) => value > 0).map((value) => value * rate);
}
What changed:
- Descriptive function name
- Clear parameter names
- Explicit TypeScript types
- Named constant for magic number
- No output argument mutation
- Useful inline documentation
The Mindset
Don't:
- Leave code worse than you found it
- Say "that's not my code"
- Wait for a dedicated refactoring sprint
- Make massive changes unrelated to your task
Do:
- Make one small improvement with every commit
- Fix what you see, even if you didn't break it
- Keep changes proportional to your task
- Leave a trail of quality improvements
AI Behavior
When working on code:
- Complete the requested task first.
- Identify at least one small cleanup opportunity.
- Apply the appropriate clean-code section from this skill.
- Note the improvement made, for example: "Also cleaned up: renamed
x to results for clarity."
When reviewing code:
- Use this full skill as the comprehensive rule set.
- Flag violations by rule number.
- Suggest incremental improvements, not complete rewrites.
Skill Orchestration
The old separate skills are now represented as sections in this one file. Use this routing table internally:
| Task | Section to apply |
|---|
| Writing/reviewing any TypeScript | Clean TypeScript: Complete Reference |
| Naming variables, functions, classes | Clean Names |
| Writing or editing comments | Clean Comments |
| Creating or refactoring functions | Clean Functions |
| Reviewing code quality | General Clean Code Principles |
| Writing or reviewing tests | Clean Tests |
The Boy Scout Promise: every piece of code you touch gets a little better. Not perfect - just better. Over time, better compounds into excellent.
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 run build)
- E2: One command to test (
npm test)
In Tandem, use the repository's package scripts: pnpm build, pnpm typecheck, pnpm lint, and pnpm test as applicable.
Functions (F1-F4)
- F1: Maximum 3 arguments (use parameter objects/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 (TypeScript style guide + 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-TS3)
These adapt the Java-specific rules (J1-J3) to TypeScript conventions:
- TS1: Keep imports explicit and stable; avoid namespace-style overuse and implicit dependencies
- TS2: Use enums or literal union types, not magic constants
- TS3: Type public interfaces explicitly and avoid
any in boundaries
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) |
| Comments | C3 | No redundant comments |
| Comments | C5 | No commented-out code |
| Functions | F1 | Max 3 arguments |
| Functions | F3 | No flag arguments |
| Functions | F4 | Delete dead functions |
| General | G5 | DRY - no duplication |
| General | G9 | Delete dead code |
| General | G16 | No obscured intent |
| General | G23 | Polymorphism over if/else |
| General | G25 | Named constants, not magic numbers |
| General | G30 | Functions do one thing |
| General | G36 | Law of Demeter (one dot) |
| Names | N1 | Descriptive names |
| Names | N5 | Name length matches scope |
| Tests | T5 | Test boundary conditions |
| Tests | T9 | Tests must be fast |
Anti-Patterns (Don't -> Do)
| Don't | Do |
|---|
| Comment every line | Delete obvious comments |
| Helper for one-liner | Inline the code |
import * as utils everywhere | Named imports for explicit dependencies |
any in public API | Specific types or unknown + narrowing |
Magic number 86400 | const SECONDS_PER_DAY = 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 |
When reviewing code, identify violations by rule number, for example: "G5 violation: duplicated logic".
When fixing or editing code, report what was fixed, for example: "Fixed: extracted magic number to SECONDS_PER_DAY (G25)."
Clean Names
N1: Choose Descriptive Names
Names should reveal intent. If a name requires a comment, it doesn't reveal its intent.
const d = 86400;
const SECONDS_PER_DAY = 86400;
function proc(values: number[]) {
return values.filter((value) => value > 0);
}
function filterPositiveNumbers(numbers: number[]) {
return numbers.filter((number) => number > 0);
}
N2: Choose Names at the Appropriate Level of Abstraction
Don't pick names that communicate implementation; choose names that reflect the level of abstraction of the class or function.
function getMapOfUserIdsToNames() {
}
function getUserDirectory() {
}
N3: Use Standard Nomenclature Where Possible
Use terms from the domain, design patterns, or well-known conventions.
class UserFactory {
create(data: unknown) {
}
}
function calculateAmortization(principal: number, rate: number, term: number) {
}
N4: Unambiguous Names
Choose names that make the workings of a function or variable unambiguous.
function rename(source: string, target: string) {
}
function renameFile(oldPath: string, newPath: string) {
}
N5: Use Longer Names for Longer Scopes
Short names are fine for tiny scopes. Longer scopes need longer, more descriptive names.
const total = numbers.reduce((sum, n) => sum + n, 0);
const MAX_RETRY_ATTEMPTS_BEFORE_FAILURE = 5;
const MAX = 5;
N6: Avoid Encodings
Don't encode type or scope information into names. Modern editors make this unnecessary.
const strName = 'Alice';
const arrUsers: string[] = [];
const nCount = 0;
const name = 'Alice';
const users: string[] = [];
const count = 0;
interface IUserRepository {
findById(id: string): Promise<unknown>;
}
interface UserRepository {
findById(id: string): Promise<unknown>;
}
N7: Names Should Describe Side Effects
If a function does something beyond what its name suggests, the name is misleading.
const configStore = new Map<string, string>();
function getConfig(configPath: string) {
if (!configStore.has(configPath)) {
configStore.set(configPath, '{}');
}
return JSON.parse(configStore.get(configPath) ?? '{}');
}
function getOrCreateConfig(configPath: string) {
if (!configStore.has(configPath)) {
configStore.set(configPath, '{}');
}
return JSON.parse(configStore.get(configPath) ?? '{}');
}
Names Quick Reference
| Rule | Principle | Example |
|---|
| N1 | Descriptive names | SECONDS_PER_DAY not d |
| N2 | Right abstraction level | getUserDirectory() not getMapOf... |
| N3 | Standard nomenclature | UserFactory, calculateAmortization |
| N4 | Unambiguous | renameFile(oldPath, newPath) |
| N5 | Length matches scope | Short for loops, long for globals |
| N6 | No encodings | users not arrUsers |
| N7 | Describe side effects | getOrCreateConfig() |
Clean Functions
F1: Too Many Arguments (Maximum 3)
function createUser(
name: string,
email: string,
age: number,
country: string,
timezone: string,
language: string,
newsletter: boolean,
) {
}
type UserData = {
name: string;
email: string;
age: number;
country: string;
timezone: string;
language: string;
newsletter: boolean;
};
function createUser(data: UserData) {
}
More than 3 arguments means your function is doing too much or needs a data structure.
F2: No Output Arguments
Don't modify arguments as side effects. Return values instead.
type Report = {
content: string;
};
function appendFooter(report: Report): void {
report.content += '\n---\nGenerated by System';
}
function withFooter(report: Report): Report {
return {
...report,
content: `${report.content}\n---\nGenerated by System`,
};
}
F3: No Flag Arguments
Boolean flags mean your function does at least two things.
function render(isTest: boolean) {
if (isTest) {
renderTestPage();
} else {
renderProductionPage();
}
}
function renderTestPage() {}
function renderProductionPage() {}
F4: Delete Dead Functions
If it's not called, delete it. No "just in case" code. Git preserves history.
Clean Comments
C1: No Inappropriate Information
Comments shouldn't hold metadata. Use Git for author names, change history, ticket numbers, and dates. Comments are for technical notes about code only.
C2: Delete Obsolete Comments
If a comment describes code that no longer exists or works differently, delete it immediately. Stale comments become "floating islands of irrelevance and misdirection."
C3: No Redundant Comments
i += 1;
user.save();
i += 1;
C4: Write Comments Well
If a comment is worth writing, write it well:
- Choose words carefully
- Use correct grammar
- Don't ramble or state the obvious
- Be brief
C5: Never Commit Commented-Out Code
Who knows how old it is? Who knows if it's meaningful? Delete it. Git remembers everything.
The Goal
The best comment is the code itself. If you need a comment to explain what code does, refactor first, comment last.
General Clean Code Principles
Critical Rules
G5: DRY (Don't Repeat Yourself)
Every piece of knowledge has one authoritative representation.
const taxRate = 0.0825;
const caTotal = subtotal * 1.0825;
const nyTotal = subtotal * 1.07;
const TAX_RATES: Record<string, number> = {CA: 0.0825, NY: 0.07};
function calculateTotal(subtotal: number, state: string): number {
return subtotal * (1 + TAX_RATES[state]);
}
G16: No Obscured Intent
Don't be clever. Be clear.
return ((x & 0x0f) << 4) | (y & 0x0f);
return packCoordinates(x, y);
G23: Prefer Polymorphism to If/Else
function calculatePay(employee: {
type: 'SALARIED' | 'HOURLY' | 'COMMISSIONED';
salary?: number;
hours?: number;
rate?: number;
base?: number;
commission?: number;
}): number {
if (employee.type === 'SALARIED') {
return employee.salary ?? 0;
} else if (employee.type === 'HOURLY') {
return (employee.hours ?? 0) * (employee.rate ?? 0);
} else if (employee.type === 'COMMISSIONED') {
return (employee.base ?? 0) + (employee.commission ?? 0);
}
return 0;
}
interface Employee {
calculatePay(): number;
}
class SalariedEmployee implements Employee {
constructor(private readonly salary: number) {}
calculatePay(): number {
return this.salary;
}
}
class HourlyEmployee implements Employee {
constructor(
private readonly hours: number,
private readonly rate: number,
) {}
calculatePay(): number {
return this.hours * this.rate;
}
}
class CommissionedEmployee implements Employee {
constructor(
private readonly base: number,
private readonly commission: number,
) {}
calculatePay(): number {
return this.base + this.commission;
}
}
G25: Replace Magic Numbers with Named Constants
if (elapsedTime > 86400) {
}
const SECONDS_PER_DAY = 86400;
if (elapsedTime > SECONDS_PER_DAY) {
}
G30: Functions Should Do One Thing
If you can extract another function, your function does more than one thing.
G36: Law of Demeter (Avoid Train Wrecks)
const outputDir = context.options.scratchDir.absolutePath;
const outputDir = context.getScratchDir();
Enforcement Checklist
When reviewing AI-generated code, verify:
- No duplication (G5)
- Clear intent, no magic numbers (G16, G25)
- Polymorphism over conditionals (G23)
- Functions do one thing (G30)
- No Law of Demeter violations (G36)
- Boundary conditions handled (G3)
- Dead code removed (G9)
Clean Tests
T1: Insufficient Tests
Test everything that could possibly break. Use coverage tools as a guide, not a goal.
test('divide', () => {
expect(divide(10, 2)).toBe(5);
});
test('divide normal', () => {
expect(divide(10, 2)).toBe(5);
});
test('divide by zero', () => {
expect(() => divide(10, 0)).toThrow(RangeError);
});
test('divide negative', () => {
expect(divide(-10, 2)).toBe(-5);
});
T2: Use a Coverage Tool
Coverage tools report gaps in your testing strategy. Don't ignore them.
vitest run --coverage
T3: Don't Skip Trivial Tests
Trivial tests document behavior and catch regressions. They're worth more than their cost.
test('user default role', () => {
const user = new User('Alice');
expect(user.role).toBe('member');
});
T4: An Ignored Test Is a Question About an Ambiguity
Don't use test.skip to hide problems. Either fix the test or delete it.
test.skip('async operation', () => {
});
test.skip('cache invalidation - requires Redis (see CONTRIBUTING.md)', () => {});
T5: Test Boundary Conditions
Bugs congregate at boundaries. Test them explicitly.
test('pagination boundaries', () => {
const items = Array.from({length: 100}, (_, i) => i);
expect(paginate(items, 1, 10)).toEqual(items.slice(0, 10));
expect(paginate(items, 10, 10)).toEqual(items.slice(90, 100));
expect(paginate(items, 11, 10)).toEqual([]);
expect(() => paginate(items, 0, 10)).toThrow(RangeError);
expect(paginate([], 1, 10)).toEqual([]);
});
T6: Exhaustively Test Near Bugs
When you find a bug, write tests for all similar cases. Bugs cluster.
test('month boundaries', () => {
expect(lastDayOfMonth(2024, 1)).toBe(31);
expect(lastDayOfMonth(2024, 2)).toBe(29);
expect(lastDayOfMonth(2023, 2)).toBe(28);
expect(lastDayOfMonth(2024, 4)).toBe(30);
expect(lastDayOfMonth(2024, 12)).toBe(31);
});
T7: Patterns of Failure Are Revealing
When tests fail, look for patterns. They often point to deeper issues.
T8: Test Coverage Patterns Can Be Revealing
Look at which code paths are untested. Often they reveal design problems.
T9: Tests Should Be Fast
Slow tests don't get run. Keep unit tests under 100ms each.
test('user creation', async () => {
const db = await connectToDatabase();
const user = await db.createUser('Alice');
expect(user.name).toBe('Alice');
});
test('user creation', async () => {
const db = new InMemoryDatabase();
const user = await db.createUser('Alice');
expect(user.name).toBe('Alice');
});
Test Organization
F.I.R.S.T. Principles
- Fast: Tests should run quickly
- Independent: Tests shouldn't depend on each other
- Repeatable: Same result every time, any environment
- Self-Validating: Pass or fail, no manual inspection
- Timely: Written before or with the code, not after
One Concept Per Test
test('user', () => {
const user = new User('Alice', 'alice@example.com');
expect(user.name).toBe('Alice');
expect(user.email).toBe('alice@example.com');
expect(user.isValid()).toBe(true);
user.activate();
expect(user.isActive).toBe(true);
});
test('user stores name', () => {
const user = new User('Alice', 'alice@example.com');
expect(user.name).toBe('Alice');
});
test('user stores email', () => {
const user = new User('Alice', 'alice@example.com');
expect(user.email).toBe('alice@example.com');
});
test('new user is valid', () => {
const user = new User('Alice', 'alice@example.com');
expect(user.isValid()).toBe(true);
});
test('user can be activated', () => {
const user = new User('Alice', 'alice@example.com');
user.activate();
expect(user.isActive).toBe(true);
});
Tests Quick Reference
| Rule | Principle |
|---|
| 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) |
Mastering Modern TypeScript
Build enterprise-grade, type-safe applications with TypeScript 5.9+.
Compatibility: TypeScript 5.9+, Node.js 22 LTS, Vite 7, NestJS 11, React 19.
Quick Start
pnpm create vite@latest my-app --template vanilla-ts
cd my-app && pnpm install
cat > tsconfig.json << 'EOF'
{
"compilerOptions": {
"target": "ES2024",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
EOF
When to Use TypeScript Mastery Rules
Use when:
- Building type-safe React, NestJS, or Node.js applications
- Migrating JavaScript codebases to TypeScript
- Implementing advanced type patterns (generics, mapped types, conditional types)
- Configuring modern TypeScript toolchains (Vite, pnpm, ESLint)
- Designing type-safe API contracts with Zod validation
- Comparing TypeScript approaches with Java or Python
Project Setup Checklist
Before starting any TypeScript project:
- [ ] Use pnpm for package management (faster, disk-efficient)
- [ ] Configure ESM-first (type: "module" in package.json)
- [ ] Enable strict mode in tsconfig.json
- [ ] Set up ESLint with @typescript-eslint
- [ ] Add Prettier for consistent formatting
- [ ] Configure Vitest for testing
Type System Quick Reference
Primitive Types
const name: string = 'Alice';
const age: number = 30;
const active: boolean = true;
const id: bigint = 9007199254740991n;
const key: symbol = Symbol('unique');
Union and Intersection Types
type Status = 'pending' | 'approved' | 'rejected';
type EmployeeRecord = Person & {employeeId: string};
type Result<T> = {success: true; data: T} | {success: false; error: string};
function handleResult<T>(result: Result<T>): T | null {
if (result.success) {
return result.data;
}
console.error(result.error);
return null;
}
Type Guards
function process(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}
interface User {
type: 'user';
name: string;
}
interface Admin {
type: 'admin';
permissions: string[];
}
function isAdmin(person: User | Admin): person is Admin {
return person.type === 'admin';
}
The satisfies Operator (TS 5.0+)
Validate type conformance while preserving inference.
const colors1 = {
red: '#ff0000',
green: '#00ff00',
} as Record<string, string>;
colors1.red.toUpperCase();
const colors2 = {
red: '#ff0000',
green: '#00ff00',
} satisfies Record<string, string>;
colors2.red.toUpperCase();
Generics Patterns
Basic Generic Function
function first<T>(items: T[]): T | undefined {
return items[0];
}
const num = first([1, 2, 3]);
const str = first(['a', 'b']);
Constrained Generics
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(item.length);
return item;
}
logLength('hello');
logLength([1, 2, 3]);
logLength(42);
Generic API Response Wrapper
interface ApiResponse<T> {
data: T;
status: number;
timestamp: Date;
}
async function fetchUser(id: string): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return {
data,
status: response.status,
timestamp: new Date(),
};
}
Utility Types Reference
| Type | Purpose | Example |
|---|
Partial<T> | All properties optional | Partial<User> |
Required<T> | All properties required | Required<Config> |
Pick<T, K> | Select specific properties | `Pick<User, "id" |
Omit<T, K> | Exclude specific properties | Omit<User, "password"> |
Record<K, V> | Object with typed keys/values | Record<string, number> |
ReturnType<F> | Extract function return type | ReturnType<typeof fn> |
Parameters<F> | Extract function parameters | Parameters<typeof fn> |
Awaited<T> | Unwrap Promise type | Awaited<Promise<User>> |
Conditional Types
type IsString<T> = T extends string ? true : false;
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Numbers = ArrayElement<number[]>;
type Strings = ArrayElement<string[]>;
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;
Mapped Types
type Immutable<T> = {
readonly [K in keyof T]: T[K];
};
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
Framework Integration
React with TypeScript
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary';
}
const Button: React.FC<ButtonProps> = ({label, onClick, variant = 'primary'}) => (
<button className={variant} onClick={onClick}>
{label}
</button>
);
const [count, setCount] = useState<number>(0);
const userRef = useRef<HTMLInputElement>(null);
NestJS with TypeScript
import {IsString, IsEmail, MinLength} from 'class-validator';
class CreateUserDto {
@IsString()
@MinLength(2)
name: string;
@IsEmail()
email: string;
}
import {z} from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
type CreateUserDto = z.infer<typeof CreateUserSchema>;
See references/react-integration.md and references/nestjs-integration.md in the original mastering-typescript skill for detailed patterns.
Validation with Zod
import {z} from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['user', 'admin', 'moderator']),
createdAt: z.coerce.date(),
});
type User = z.infer<typeof UserSchema>;
function parseUser(data: unknown): User {
return UserSchema.parse(data);
}
const result = UserSchema.safeParse(data);
if (result.success) {
console.log(result.data);
} else {
console.error(result.error.issues);
}
Modern Toolchain (2025)
| Tool | Version | Purpose |
|---|
| TypeScript | 5.9+ | Type checking and compilation |
| Node.js | 22 LTS | Runtime environment |
| Vite | 7.x | Build tool and dev server |
| pnpm | 9.x | Package manager |
| ESLint | 9.x | Linting with flat config |
| Vitest | 3.x | Testing framework |
| Prettier | 3.x | Code formatting |
ESLint Flat Config (ESLint 9+)
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.strictTypeChecked, {
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
});
Migration Strategies
Incremental Migration
- Add
allowJs: true and checkJs: false to tsconfig.json.
- Rename files from
.js to .ts one at a time.
- Add type annotations gradually.
- Enable stricter options incrementally.
JSDoc for Gradual Typing
function createUser(name, age) {
return {name, age};
}
See references/enterprise-patterns.md in the original mastering-typescript skill for comprehensive migration guides.
Common Mistakes
| Mistake | Problem | Fix |
|---|
Using any liberally | Defeats type safety | Use unknown and narrow |
| Ignoring strict mode | Misses null/undefined bugs | Enable all strict options |
Type assertions (as) | Can hide type errors | Use satisfies or guards |
| Enum for simple unions | Generates runtime code | Use literal unions instead |
| Not validating API data | Runtime type mismatches | Use Zod at boundaries |
Cross-Language Comparison
| Feature | TypeScript | Java | Python |
|---|
| Type System | Structural | Nominal | Gradual (duck typing) |
| Nullability | Explicit (`T | null`) | @Nullable annotations |
| Generics | Type-level, erased | Type-level, erased | Runtime via typing |
| Interfaces | Structural matching | Must implement | Protocol (3.8+) |
| Enums | Avoid (use unions) | First-class | Enum class |
Reference Files From Mastering TypeScript
references/type-system.md - Complete type system guide
references/generics.md - Advanced generics patterns
references/enterprise-patterns.md - Error handling, validation, architecture
references/react-integration.md - React + TypeScript patterns
references/nestjs-integration.md - NestJS API development
references/toolchain.md - Modern build tools configuration
Assets From Mastering TypeScript
assets/tsconfig-template.json - Strict enterprise config
assets/eslint-template.js - ESLint 9 flat config
Scripts From Mastering TypeScript
scripts/validate-setup.sh - Verify TypeScript environment
Final Tandem Checklist
Before finishing a coding task in Tandem:
- Confirm the requested behavior is implemented.
- Confirm code respects Tandem's one-class-per-file and shared-boundary organization.
- Confirm touched code has logical blank-line separation.
- Confirm names reveal intent and side effects.
- Confirm functions have at most 3 arguments unless a parameter object is used.
- Confirm functions do one thing and avoid flags/output arguments.
- Confirm comments explain why, not what, and no commented-out code remains.
- Confirm duplication, magic numbers, dead code, and train wrecks were removed where adjacent.
- Confirm boundary conditions are handled.
- Confirm tests mirror source structure under
tests/, are fast, focused, and cover edge cases near the change.
- Confirm TypeScript boundaries avoid
any, prefer unknown plus narrowing, and use Zod/runtime validation for untrusted data.
- Run the smallest relevant verification command, then broader checks when the change warrants it.