Plato would love Typescript Generics!

Nathan Clement · September 8, 2026 · 6 min read

The Ancient Art of Software Design: A Philosophical Perspective

Midjourney prompt: plato software dev laptop teaching athens :: classical artwork painting

Have you ever found yourself meticulously outlining a software system’s architecture with your team, only to get derailed in a debate about the details of actual code? It happens all the time, and it’s source hearkens all the way back to ideas of the ancient Greek philosophers — the conflict between details of “implementation” and the more zoomed out concept of “form”

Implementation versus Form

Seasoned engineers recommend segregating architecture from implementation from the start. Lay down high level plans first: detailing your system’s purpose, functionality, and essential properties. Only then should you dive into the code, breathing life into your high level designs.

This is much easier said than done! Limited resources, tight deadlines, or complex problems often nudge us towards a design that is more focsued on code-level specifics than architectural integrity.

How can we resolve this conflict? Turns out: these problems have been around far longer than the field of software engineering…

Plato’s Philosophy and Modern Software Design

Thousands of years ago, Plato developed his Theory of Forms. In this theory, a “Form” or εἶδος (eidos) serves as the abstract, ideal concept of a thing. It’s the answer to the question, “What is that?”

For example, if I point to a computer and ask, “What is that?” you’d respond with, “A computer.”

Now, in the real world, millions of individual computers exist, but none of them individually define what it means to be a computer. They all participate in the Essential Form of “computerness.”

So how would Plato approach building a computer? He wouldn’t start by fretting over the material for the keyboard or the specs of the microprocessor. Instead, he would ponder fundamental questions like, “Should it be portable?”, “What’s the ideal battery life?”, or “What kind of tasks should it accomplish for the user?”

By identifying the Essential Form of a computer before diving into the nitty-gritty details, an engineer sets themselves up for success. This approach minimizes the risk of investing months crafting a gold keyboard, only to realize later that what users really wanted was backlighting.

The Power of TypeScript Generics: Bridging the Gap

Typescript Generics allow us to write code that is both abstract and strongly typed, serving as a bridge between our ideal architecture (it’s Platonic Form), and it’s concrete implementation. They force us to consider a type’s essential characteristics, allowing us to write more abstract, flexible, and maintainable code, without compromising type safety.

Imagine you are building a task scheduler. The tasks are objects, and they have different priority levels. A basic task can be modeled like so:

// The interface for a "Task"
// can be thought of as it's "Form"
class Task {
  id: string;
  name: string;
  priority: number;
  category: "feature" | "bug" | "chore";
  getBranchName () => `${this.category}-${this.id}/${this.name}`;
}

The scheduler will need a way to handle task priorities. For this, we will use a PriorityQueue

We could do something like this:

class BadPriorityQueue {
  private queue: any[] = [];

  addTaskToTopPriority(task: any): void {
    this.queue.unshift(task);
  }

  removeTask(): any {
    return this.queue.shift();
  }

  sortPriorities(): void {
    this.queue.sort((a, b) => {
      if (typeof a.priority !== 'number' || typeof b.priority !== 'number') {
        return 0;
      }
      return b.priority - a.priority;
    });
  }
}

This will work just fine for our use case, but what happens if we want to build a Priority Queue for something other than a Task? We will end up having to write everything all over again, and have another very similar piece of code in our codebase.

If we had been thinking in terms of what the “Form” of a Priority Queue really was, instead of jumping straight into building, we could have avoided this problem.

Consider the following:

abstract class ObjectWithPriority {
  priority: number
}

class PriorityQueue<T extends ObjectWithPriority> {
  private queue: T[] = [];
  
  enqueue(item: T): void {
    this.queue.push(item);
    this.queue.sort((a, b) => b.priority - a.priority);
  }

  dequeue(): T | undefined {
    return this.queue.shift();
  }
}

Notice the usage of <T> ? In this class T extends ObjectWithPriority ensures that you can’t put any random obect in the priority queue. It has to have a numeric priority field. This aligns with Plato’s theory, establishing a form ObjectWithPriority that every element in the queue must aspire too. Additionally, we now know that a PriorityQueue in it’s essential form is something that looks at a bunch of things with priorities, and can both enqueue and dequeue them.

Putting it all together

At this point, we have defined two forms: a Task and a PriorityQueue They work together seamlessly, and give us Type-safety for free.

// Create your queue, letting typescript know that your ObjectWithAPriority
// Will be a "Task" class
const myPriorityQueue = new PriorityQueue<Task>();

const task1 = new Task();
task1.id = "1";
task1.name = "ImplementFeatureX";
task1.priority = 1;
task1.category = "feature";

const task2 = new Task();
task2.id = "2";
task2.name = "FixBugY";
task2.priority = 10;
task2.category = "bug";

myPriorityQueue.enqueue(task1);
myPriorityQueue.enqueue(task2);

const nextTask = myPriorityQueue.dequeue();
if (nextTask) {
  console.log(`Next task to execute: ${nextTask.name}, branch: ${nextTask.getBranchName()}`); 
  // Output: "Next task to execute: FixBugY, branch: bug-2/FixBugY"
}

Furthermore, we can go on to add new features to our Tasks, and as long as they never violate the requirements of ObjectWithPriority we know that we will not break our queue system.

Additionally, we can modify our queue system while knowing that it will not require any code changes for tasks!

We can even add a new PriorityQueue<CustomerFeatureRequest> without having to rewrite any code for the queue (as long as a CustomerFeatureRequest has a numeric priority).

Unlocking Better System Design with Philosophical Insight

When Plato spoke about forms, little did he know that his ideas would find applications in modern system design. By aligning our system’s architecture closely with its Platonic form, we achieve a design that is robust, flexible, and future-proof.

In applying TypeScript Generics, we practically implement the philosophical insights of Plato, grounding his age-old wisdom in the real-world scenarios of software development. It’s a fascinating blend of philosophy and technology, one that not only enriches our understanding of software design but also brings an age-old debate into the very code we write every day.


So, what’s your take? Can Plato’s Theory of Forms help you focus better on architectural aspects, or is this just philosophical musings with little real-world utility? I’m eager to hear your thoughts!


Want to learn more about TypesScript generics?

Check out their documentation here: https://www.typescriptlang.org/docs/handbook/2/generics.html

And here is a great article discusssing them in depth: https://rossbulat.medium.com/typescript-generics-explained-15c6493b510f

Want to learn more about Plato and philosophy?

Check out the Wikipedia article on Plato’s Theory of Forms here: https://en.wikipedia.org/wiki/Theory_of_forms

Also, Stanford’s Encyclopedia of Philosophy is a wonderful resource: https://plato.stanford.edu/entries/plato/#DoePlaChaHisMinAboFor


Originally published on 2023-09-04 on Medium.