TypeScript Design Patterns: When Anime Accidentally Teaches You To Code
Ever notice how your favorite anime moments follow the exact same patterns as good software design? That's not a coincidence.
Before We Start: What You Need
Prerequisites: Solid JavaScript/TypeScript basics (classes, interfaces, OOP). If you're still figuring out const
vs let
, hit the official TypeScript docs first.
The Promise: By the end, you'll see code patterns everywhere in anime - and remember them forever because of it.
1. Observer Pattern: Death Note's Notification System
Picture this: Light writes a name in the Death Note. Instantly, every shinigami in the death realm gets notified. Ryuk starts laughing, the death registry updates, other death gods take notes. One action, multiple reactions - all happening automatically.
That's the Observer pattern in its purest form.
interface ShinigamiObserver {
onDeath(victim: string, cause: string): void
}
class DeathNote {
private observers = new Set<ShinigamiObserver>()
subscribe(shinigami: ShinigamiObserver) {
this.observers.add(shinigami)
}
unsubscribe(shinigami: ShinigamiObserver) {
this.observers.delete(shinigami)
}
writeVictim(name: string, cause: string = 'heart attack') {
console.log(`π '${name}' written in Death Note...`)
this.observers.forEach(observer => {
try {
observer.onDeath(name, cause) // β
Fixed: was 'victim', now 'name'
} catch (err) {
console.error('β οΈ Shinigami observation failed:', err)
}
})
}
}
class Ryuk implements ShinigamiObserver {
onDeath(victim: string, cause: string) {
console.log(`π Ryuk chuckles: 'Humans are so entertaining...'`)
}
}
class DeathRegistry implements ShinigamiObserver {
onDeath(victim: string, cause: string) {
console.log(`π Registry updated: ${victim} - ${cause}`)
}
}
// Usage
const deathNote = new DeathNote()
deathNote.subscribe(new Ryuk())
deathNote.subscribe(new DeathRegistry())
deathNote.writeVictim('L', 'heart attack')
Why This Matters In Real Code:
- Event systems: Button clicks, API responses, WebSocket messages
- Model-view updates: React state changes triggering multiple component re-renders
- Notification systems: User actions triggering emails, push notifications, and analytics
When NOT to use: Simple one-to-one relationships, performance-critical loops (observer iteration overhead).
Testing reality: Easy to mock observers and verify they receive events, but debugging cascading updates when observers trigger more events is absolute hell.
2. Strategy Pattern: Pokemon Type Effectiveness
Pokemon don't hardcode damage calculations. Charizard doesn't have an if (enemy === 'grass') damage *= 2
buried somewhere in its attack method. Instead, it has a Fire Strategy that knows how to handle all type matchups.
When Arceus changes types with different plates? Same Pokemon, different strategy object. Clean, swappable, testable.
interface ElementalStrategy {
calculateDamage(basePower: number, targetType: string): number
}
class FireStrategy implements ElementalStrategy {
private effectiveness = {
grass: 2.0,
water: 0.5,
fire: 0.5,
normal: 1.0
}
calculateDamage(basePower: number, targetType: string): number {
const multiplier = this.effectiveness[targetType] || 1.0
return basePower * multiplier
}
}
class WaterStrategy implements ElementalStrategy {
private effectiveness = {
fire: 2.0,
grass: 0.5,
water: 0.5,
normal: 1.0
}
calculateDamage(basePower: number, targetType: string): number {
const multiplier = this.effectiveness[targetType] || 1.0
return basePower * multiplier
}
}
class Pokemon {
constructor(
private name: string,
private elementalStrategy: ElementalStrategy
) {}
changeType(newStrategy: ElementalStrategy) {
this.elementalStrategy = newStrategy
console.log(`${this.name} changed type!`)
}
attack(targetType: string, movePower: number = 50) {
const damage = this.elementalStrategy.calculateDamage(movePower, targetType)
console.log(`${this.name} deals ${damage} damage!`)
return damage
}
}
const charizard = new Pokemon('Charizard', new FireStrategy())
charizard.attack('grass')
charizard.attack('water')
charizard.changeType(new WaterStrategy())
charizard.attack('fire')
Real-World Power:
- Payment processing: PayPal vs Stripe vs credit card - same checkout flow, different payment strategies
- A/B testing: Swap recommendation algorithms without changing the product page
- Data sorting: QuickSort vs MergeSort vs HeapSort based on data size
Skip it when: You only have 1-2 variants (total overkill), or algorithms need to share complex internal state.
Testing reality: Each strategy tests in isolation - absolutely beautiful for unit tests. You can inject mock strategies for testing edge cases.
3. Command Pattern: JoJo Stand Abilities
Every Stand ability is literally a Command object! Star Platinum's "ORA ORA" rush? That's a command with execute()
. Crazy Diamond's restoration? Command with both execute()
and undo()
. Gold Experience's life creation? Another command.
Jotaro doesn't directly punch things - he queues up ORA commands and Star Platinum executes them in sequence.
interface StandCommand {
execute(): void
undo?(): void
priority?: number
}
class OraPunch implements StandCommand {
priority = 1
constructor(private target: string, private damage: number) {}
execute() {
console.log(`β ORA! Star Platinum punches ${this.target} for ${this.damage} damage!`)
}
}
class CrazyRestore implements StandCommand {
priority = 2
private restored = false
constructor(private target: string) {}
execute() {
console.log(`π DORA! Crazy Diamond restores ${this.target}!`)
this.restored = true
}
undo() {
if (this.restored) {
console.log(`π₯ ${this.target} breaks again!`)
this.restored = false
}
}
}
class StandUser {
private commandQueue: StandCommand[] = []
private executedCommands: StandCommand[] = []
queueAbility(command: StandCommand) {
this.commandQueue.push(command)
this.commandQueue.sort((a, b) => (b.priority || 0) - (a.priority || 0))
}
executeAll() {
this.commandQueue.forEach(cmd => {
cmd.execute()
this.executedCommands.push(cmd)
})
this.commandQueue = []
}
undoLast() {
const lastCommand = this.executedCommands.pop()
if (lastCommand?.undo) {
lastCommand.undo()
}
}
}
const josuke = new StandUser()
josuke.queueAbility(new OraPunch('Angelo', 50))
josuke.queueAbility(new CrazyRestore('broken wall'))
josuke.executeAll() // Restoration happens first (higher priority)
josuke.undoLast() // Wall breaks again!
Real-World Applications:
- Undo/redo systems: Text editors, image editors, any app with Ctrl+Z
- Macro recording: Recording user actions and replaying them
- GUI actions: Button clicks become command objects for easier testing
- Database transactions: Each operation as a command, rollback via undo
Skip it when: Simple direct method calls, memory-constrained environments (commands create lots of objects).
Testing reality: Absolutely fantastic for testing - verify commands were created correctly without executing them. Test undo logic separately from execution logic.
4. Factory Pattern: Naruto Summoning Contracts
Nobody manually creates Gamabunta or Manda with new GiantToad()
. You perform the summoning jutsu (factory method) and the appropriate creature appears based on your contract (registry) and chakra level.
The factory handles all the complex logic: "Does this ninja have a toad contract? How much chakra do they have? Create the right size summon."
abstract class SummonedCreature {
abstract size: 'small' | 'medium' | 'boss'
abstract attack(): void
abstract element: string
}
class Gamabunta extends SummonedCreature {
size = 'boss'
element = 'water'
attack() {
console.log('πΈ Gamabunta spits a massive water bullet!')
}
}
class Gamakichi extends SummonedCreature {
size = 'medium'
element = 'water'
attack() {
console.log('πΈ Gamakichi launches water shuriken!')
}
}
class Manda extends SummonedCreature {
size = 'boss'
element = 'earth'
attack() {
console.log('π Manda burrows and strikes from underground!')
}
}
class SummoningContractFactory {
private contracts: Record<string, Array<new () => SummonedCreature>> = {}
constructor() {
this.contracts.toad = [Gamakichi, Gamabunta]
this.contracts.snake = [Manda]
}
signContract(animal: string, creatures: Array<new () => SummonedCreature>) {
this.contracts[animal] = creatures
}
summon(contract: string, chakraLevel: 'low' | 'medium' | 'high'): SummonedCreature {
const availableCreatures = this.contracts[contract]
if (!availableCreatures?.length) {
throw new Error(`No ${contract} contract signed!`)
}
const sizeMap = { low: 'small', medium: 'medium', high: 'boss' }
const targetSize = sizeMap[chakraLevel]
const CreatureClass = availableCreatures.find(CreatureClass => {
const temp = new CreatureClass()
return temp.size === targetSize
}) || availableCreatures[0]
return new CreatureClass()
}
}
const naruto = new SummoningContractFactory()
const gamabunta = naruto.summon('toad', 'high')
gamabunta.attack()
class Katsuyu extends SummonedCreature {
size = 'boss'
element = 'healing'
attack() {
console.log('π Katsuyu releases healing acid!')
}
}
naruto.signContract('slug', [Katsuyu])
Factory Power In Real Apps:
- Database connections: MySQL vs PostgreSQL vs MongoDB - same interface, different implementations
- HTTP clients: Axios vs Fetch vs custom - factory picks based on environment
- UI components: Button factory that creates iOS vs Android vs Web buttons
- Plugin systems: Load different payment processors, authentication providers, etc.
Don't use when: Simple object creation, only one type of object, tight coupling is actually fine.
Testing reality: Tricky because factories hide construction details. Use dependency injection to pass in mock factories for testing.
5. Singleton Pattern: One Piece Devil Fruit Powers
Here's the thing about Devil Fruits: each power can only exist once in the entire world. When Luffy eats the Gomu Gomu fruit, nobody else can have rubber powers until he dies and the fruit respawns somewhere else.
That's Singleton pattern - enforcing "only one instance can exist."
class DevilFruit {
private static activePowers = new Map<string, DevilFruit>()
constructor(private powerName: string, private user: string) {
if (DevilFruit.activePowers.has(powerName)) {
const currentUser = DevilFruit.activePowers.get(powerName)?.user
throw new Error(`${powerName} power already belongs to ${currentUser}!`)
}
DevilFruit.activePowers.set(powerName, this)
Object.freeze(this)
console.log(`π ${user} gained ${powerName} powers!`)
}
use() {
console.log(`${this.user} uses ${this.powerName}!`)
}
static releasePower(powerName: string) {
if (DevilFruit.activePowers.delete(powerName)) {
console.log(`π ${powerName} power is available again...`)
}
}
static getActivePowers() {
return Array.from(DevilFruit.activePowers.entries()).map(
([power, fruit]) => `${power}: ${fruit.user}`
)
}
}
const luffyPower = new DevilFruit('Gomu Gomu', 'Luffy')
luffyPower.use()
try {
const fakeLuffy = new DevilFruit('Gomu Gomu', 'Fake Luffy')
} catch (e) {
console.log(e.message) // "Gomu Gomu power already belongs to Luffy!"
}
DevilFruit.releasePower('Gomu Gomu')
const newUser = new DevilFruit('Gomu Gomu', 'Someone New')
Legitimate Singleton Use Cases:
- Database connection pools: Expensive to create, should be shared
- Logging systems: One logger instance for the entire app
- Configuration objects: App settings that should be globally accessible
- Cache managers: One cache instance to rule them all
Skip it when: Most of the time! Creates hidden dependencies, makes testing a nightmare, breaks inversion of control.
Testing reality: Absolutely terrible - global state makes tests interdependent and impossible to parallelize. Consider dependency injection instead.
6. Template Method Pattern: Shounen Training Arcs
Every shounen training arc follows the exact same structure: Hero arrives β meets mentor β struggles with impossible training β has dramatic breakthrough β powers up β leaves to fight the real enemy.
Dragon Ball, Naruto, Bleach, One Piece - they all use this template. The steps never change, but the specific details are wildly different.
abstract class TrainingArc {
complete() {
this.arrive()
this.meetMentor()
this.struggle()
this.breakthrough()
this.powerUp()
this.depart()
}
protected arrive() {
console.log('πΆ Hero arrives at mysterious training location...')
}
protected meetMentor() {
console.log('π΄ A wise but eccentric mentor appears...')
}
protected abstract struggle(): void
protected abstract breakthrough(): void
protected powerUp() {
console.log('β¨ Power level increases dramatically!')
}
protected depart() {
console.log('π Time to test new powers against the real enemy!\n')
}
}
class DragonBallTraining extends TrainingArc {
protected struggle() {
console.log('πͺ Goku trains under 100x Earth gravity!')
console.log('π€ Body gets crushed repeatedly, bones breaking...')
}
protected breakthrough() {
console.log('β‘ Goku\'s hair turns golden - Super Saiyan achieved!')
console.log('π₯ I am the hope of the universe!')
}
}
class NarutoTraining extends TrainingArc {
protected struggle() {
console.log('π Naruto fails to form Rasengan 1000+ times...')
console.log('π Hands get shredded from chakra control mistakes...')
}
protected breakthrough() {
console.log('πͺοΈ Perfect Rasengan forms in Naruto\'s palm!')
console.log('π¦ I never go back on my word - that\'s my ninja way!')
}
}
class BleachTraining extends TrainingArc {
protected struggle() {
console.log('βοΈ Ichigo fights his inner Hollow for days...')
console.log('πΉ Dark Ichigo taunts him mercilessly...')
}
protected breakthrough() {
console.log('π Ichigo masters his Hollow mask!')
console.log('π I\'ll protect everyone with my own power!')
}
}
console.log('=== Dragon Ball Training Arc ===')
new DragonBallTraining().complete()
console.log('=== Naruto Training Arc ===')
new NarutoTraining().complete()
console.log('=== Bleach Training Arc ===')
new BleachTraining().complete()
Template Method In Real Development:
- Data processing pipelines: Load β validate β transform β save (details vary per data type)
- Authentication flows: Check credentials β validate permissions β grant access (OAuth vs JWT vs API keys)
- Testing frameworks: Setup β run test β cleanup β report (Jest, Mocha, etc.)
- Build processes: Compile β bundle β optimize β deploy (different for each framework)
Skip it when: Highly dynamic workflows where steps change based on runtime conditions, or when inheritance feels forced.
Testing reality: Excellent - test the template separately from implementations, and test each step in isolation. Easy to mock specific steps.
The Big Picture: Why These 6 Patterns Rule
These aren't random patterns picked from a textbook - they're the essential toolkit that covers 80% of real design problems you'll face:
- Observer: When one thing needs to tell many things (events, notifications)
- Strategy: When you need swappable algorithms (payment methods, sorting algorithms)
- Command: When actions become objects (undo/redo, macros, queuing)
- Factory: When creation gets complex (database connections, plugins)
- Singleton: When you truly need "only one" (rarely, but sometimes legitimately)
- Template Method: When steps stay the same but details change (workflows, pipelines)
Notice how they complement each other? You might use a Factory to create different Strategies, then queue them up as Commands, with Observers listening for results - all within a Template Method workflow.
Your Next Steps: From Anime Fan to Pattern Master
- Pick the pattern that clicked hardest - which anime example made you go "holy shit, that's brilliant"?
- Find it in your current codebase - I guarantee it's already there, just implemented messily with if/else chains or copy-pasta code.
- Refactor one small piece using the pattern properly - don't boil the ocean, just clean up one method or class.
- Feel the difference in code clarity, testability, and maintainability - once you experience clean pattern usage, you can't go back.
- Start seeing patterns everywhere - in other anime, in your daily apps, in frameworks you use. They're literally everywhere once your brain is trained to spot them.
The goal isn't to force patterns into every line of code - it's to recognize when you're solving the same fundamental problems that anime characters face, just in TypeScript instead of chakra and Stand battles.
Additional Resources:
- Refactoring Guru Design Patterns - Best visual explanations
- TypeScript Official Docs - Master the fundamentals first
- MDN JavaScript Guide - Core language concepts
- GeeksforGeeks TypeScript Patterns - More examples
- FreeCodeCamp Design Patterns - Free deep dives
Now go forth and code something legendary - your favorite anime heroes would be proud! π
Top comments (0)