Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add fredericvilcot/spectre-agents --skill "typescript-craft"
Install specific skill from multi-skill repository
# Description
Apply software craftsmanship principles to TypeScript code: type safety, functional patterns, clean architecture, and best practices
# SKILL.md
name: typescript-craft
description: "Apply software craftsmanship principles to TypeScript code: type safety, functional patterns, clean architecture, and best practices"
context: fork
agent: software-craftsman
allowed-tools: Read, Grep, Glob, Edit, Write
TypeScript Craft Skill
You are applying software craftsmanship principles to TypeScript code. Follow these guidelines rigorously.
Type Safety First
Strict Configuration
Always assume and enforce strict TypeScript configuration:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true
}
}
Avoid Type Escapes
- Never use
any- useunknownand narrow with type guards - Never use
asassertions unless absolutely necessary (and document why) - Never use
!non-null assertions - handle null/undefined explicitly - Avoid
@ts-ignore- fix the type issue instead
Leverage the Type System
// Bad: primitive obsession
function createUser(name: string, email: string, age: number) {}
// Good: domain types
type UserName = Brand<string, 'UserName'>
type Email = Brand<string, 'Email'>
type Age = Brand<number, 'Age'>
function createUser(name: UserName, email: Email, age: Age) {}
Algebraic Data Types
Discriminated Unions for Domain Modeling
// Model all possible states explicitly
type AsyncData<T, E = Error> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: E }
// Result type for error handling
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E }
Exhaustive Pattern Matching
function handleState<T>(state: AsyncData<T>): string {
switch (state.status) {
case 'idle': return 'Not started'
case 'loading': return 'Loading...'
case 'success': return `Data: ${state.data}`
case 'error': return `Error: ${state.error.message}`
default: {
const _exhaustive: never = state
return _exhaustive
}
}
}
Functional Patterns
Immutability by Default
// Use readonly everywhere
type User = Readonly<{
id: UserId
name: string
roles: readonly Role[]
}>
// Prefer const assertions
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
} as const
Pure Functions
// Bad: side effects, mutation
function processUsers(users: User[]) {
users.sort((a, b) => a.name.localeCompare(b.name))
return users
}
// Good: pure, no mutation
function processUsers(users: readonly User[]): User[] {
return [...users].sort((a, b) => a.name.localeCompare(b.name))
}
Function Composition
// Small, composable functions
const pipe = <T>(...fns: Array<(arg: T) => T>) =>
(value: T): T => fns.reduce((acc, fn) => fn(acc), value)
const processUser = pipe(
validateUser,
normalizeEmail,
hashPassword,
)
Clean Architecture in TypeScript
Domain Layer (No Dependencies)
// domain/user.ts - Pure domain logic
export type UserId = Brand<string, 'UserId'>
export type User = Readonly<{
id: UserId
email: Email
name: UserName
}>
export type UserRepository = {
findById(id: UserId): Promise<Result<User, UserNotFoundError>>
save(user: User): Promise<Result<void, PersistenceError>>
}
Application Layer (Use Cases)
// application/create-user.ts
type CreateUserDeps = {
userRepository: UserRepository
emailService: EmailService
}
type CreateUserCommand = {
email: string
name: string
}
export const createUser = (deps: CreateUserDeps) =>
async (command: CreateUserCommand): Promise<Result<User, CreateUserError>> => {
// Business logic here
}
Infrastructure Layer (Adapters)
// infrastructure/postgres-user-repository.ts
export const createPostgresUserRepository = (db: Database): UserRepository => ({
findById: async (id) => {
// Implementation detail
},
save: async (user) => {
// Implementation detail
},
})
Error Handling
Never Throw for Expected Errors
// Bad: throwing for business errors
function divide(a: number, b: number): number {
if (b === 0) throw new Error('Division by zero')
return a / b
}
// Good: explicit error type
function divide(a: number, b: number): Result<number, DivisionByZeroError> {
if (b === 0) return { ok: false, error: new DivisionByZeroError() }
return { ok: true, value: a / b }
}
Typed Errors
// Define specific error types
class UserNotFoundError extends Error {
readonly _tag = 'UserNotFoundError'
constructor(readonly userId: UserId) {
super(`User not found: ${userId}`)
}
}
class ValidationError extends Error {
readonly _tag = 'ValidationError'
constructor(readonly field: string, readonly reason: string) {
super(`Validation failed for ${field}: ${reason}`)
}
}
type CreateUserError = UserNotFoundError | ValidationError | PersistenceError
Testing Patterns
Arrange-Act-Assert with Type Safety
describe('createUser', () => {
it('should create a valid user', async () => {
// Arrange
const deps: CreateUserDeps = {
userRepository: createInMemoryUserRepository(),
emailService: createMockEmailService(),
}
const command: CreateUserCommand = {
email: '[email protected]',
name: 'Test User',
}
// Act
const result = await createUser(deps)(command)
// Assert
expect(result.ok).toBe(true)
if (result.ok) {
expect(result.value.email).toBe(command.email)
}
})
})
Test Factories
// test/factories/user.factory.ts
export const createTestUser = (overrides?: Partial<User>): User => ({
id: 'user-123' as UserId,
email: '[email protected]' as Email,
name: 'Test User' as UserName,
...overrides,
})
Code Style
Naming Conventions
- Types/Interfaces:
PascalCase - Functions/Variables:
camelCase - Constants:
SCREAMING_SNAKE_CASEfor true constants,camelCasefor const bindings - Files:
kebab-case.tsfor modules,PascalCase.tsfor classes/components
File Structure
src/
βββ domain/ # Pure domain logic, no dependencies
β βββ user/
β β βββ user.ts
β β βββ user.repository.ts
β β βββ user.errors.ts
βββ application/ # Use cases, orchestration
β βββ user/
β βββ create-user.ts
βββ infrastructure/ # External adapters
β βββ persistence/
β βββ http/
βββ shared/ # Cross-cutting utilities
βββ types/
βββ utils/
When Reviewing TypeScript Code
Check for:
1. Type safety violations (any, assertions, !)
2. Proper error handling (Result types vs exceptions)
3. Immutability (readonly, no mutations)
4. Pure functions (no side effects)
5. Domain modeling (proper use of types)
6. Separation of concerns (layers respected)
7. Test coverage and quality
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.