Skip to content

RESOURCES / BLOG

What is Polymorphism in JavaScript?

You have probably seen threads where someone asks how to design flexible JavaScript APIs that work with multiple object types without a forest of if statements. That topic usually lands on one core concept from object oriented programming and functional design: polymorphism.

Hi folks,

I keep hearing about polymorphism in JavaScript, especially in relation to classes and “duck typing.” I use JS daily but I am not sure I can explain it clearly or choose the right pattern when building APIs.

What is polymorphism in JavaScript? How does it work with classes and prototypes, what are practical patterns to emulate function overloading, and what are good examples or pitfalls to watch for?

Thanks!

Polymorphism means “many forms.” In code, it’s one of the four pillars of object-oriented programming. It lets you write one interface and swap in different implementations without changing the calling code.

JavaScript supports polymorphism through class inheritance, prototypal objects, and dynamic typing. The result is more extensible code and fewer conditionals scattered across your app. Let’s break it down:

With classes, you define a common contract and let subclasses provide their own behavior. Callers depend on the contract, not the concrete type.

class Shape {
  area() { throw new Error("Implement in subclass"); }
}

class Rectangle extends Shape {
  constructor(w, h) { super(); this.w = w; this.h = h; }
  area() { return this.w * this.h; }
}

class Circle extends Shape {
  constructor(r) { super(); this.r = r; }
  area() { return Math.PI * this.r * this.r; }
}

function printArea(shape) {
  // Works with any object that honors the Shape interface
  console.log(shape.area().toFixed(2));
}

[ new Rectangle(3, 4), new Circle(2) ].forEach(printArea);Code language: JavaScript (javascript)

In JavaScript, you do not need a class hierarchy. If an object has the right methods, treat it as compatible. This is duck typing: “if it quacks like a duck…”

const sendPing = (target) => {
  if (target && typeof target.send === "function") {
    return target.send("ping");
  }
  throw new TypeError("Object must implement send(msg)");
};

const WebSocketClient = { send: (msg) => console.log("ws:", msg) };
const MockClient = { send: (msg) => console.log("mock:", msg) };

[WebSocketClient, MockClient].forEach(sendPing);Code language: JavaScript (javascript)

JavaScript does not have traditional overloading, but you can branch on argument types or use an options object. Keep branches small and validated.

// One function, multiple input shapes
function read(input) {
  if (typeof input === "string") {
    return fetch(input).then(r => r.text());
  }
  if (input instanceof Blob) {
    return input.text();
  }
  throw new TypeError("read accepts a URL string or a Blob");
}Code language: JavaScript (javascript)
  • Prefer small, well-named helpers over one giant switch.
  • Validate types at runtime, and document supported shapes.
  • Consider TypeScript to make contracts explicit.

The Strategy pattern is a practical use of polymorphism. Define a shared interface, then swap strategies at runtime without changing callers.

class Thumbnailer {
  make(/* asset */) { throw new Error("Implement in subclass"); }
}

class LocalThumbnailer extends Thumbnailer {
  make(asset) { return `file://${asset.path}`; }
}

class CloudThumbnailer extends Thumbnailer {
  // Example URL-based thumbnail; the caller does not care how it is built
  make(asset) {
    const base = "https://res.cloudinary.com/demo/image/upload/";
    const t = "c_fill,w_300,h_300,q_auto,f_auto";
    return `${base}${t}/${asset.publicId}.${asset.ext}`;
  }
}

function buildThumbnails(assets, service) {
  return assets.map(a => service.make(a));
}

const assets = [
  { publicId: "sample", ext: "jpg", path: "/tmp/sample.jpg" },
  { publicId: "logo", ext: "png", path: "/tmp/logo.png" }
];

console.log(buildThumbnails(assets, new LocalThumbnailer()));
console.log(buildThumbnails(assets, new CloudThumbnailer()));Code language: JavaScript (javascript)
  • Respect substitution: If A is expected, B should work if it follows A’s contract.
  • Prefer composition over deep inheritance trees.
  • For format-specific behavior, isolate it behind interfaces. For example, JPEG vs PNG choices have tradeoffs like transparency support and compression; see this overview.

If you manage media, a polymorphic design lets you swap processing backends. In the example above, the CloudThumbnailer returns an image URL that includes on-the-fly transformations. Since a Cloudinary URL is just an image URL, the rest of your app can treat it like any other source. You can decide at runtime which strategy to use based on deployment or content type, while still keeping a single interface.

  • Polymorphism lets one interface support multiple implementations.
  • In JS you get it through classes, prototypes, and duck typing.
  • Emulate overloading with type checks or options objects; validate inputs.
  • Use the Strategy pattern to swap behaviors without changing callers, for example local vs. URL-based thumbnail generation.

Ready to build faster, cleaner media workflows in your apps? Create your free Cloudinary account and start optimizing today.

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free