Dataslope logoDataslope

Generic Abstractions and Type Classes

Parametric polymorphism, ad-hoc polymorphism, and the type-class-as-record pattern that fits TypeScript.

Functional programmers love generic code — code that works for every shape of data, not just one. That's not laziness or showing-off; it's a real engineering discipline: when a function is generic, it has fewer ways to misbehave, because it knows less about its arguments.

Parametric polymorphism: "I don't know, and I don't care"

The simplest generic function: identity.

Code Block
TypeScript 5.7

id is fully generic — its type <A>(a: A) => A doesn't constrain A at all. Because it knows nothing about A, the only thing it can do is return its argument. That's a powerful property: from the type alone, you can prove what the function does.

This is a deep idea from category theory called parametricity: the more polymorphic a function is, the fewer behaviors are compatible with its type. A function <A>(xs: A[]) => A[] might reverse, sort by some external order, dedupe, or do nothing — but it cannot invent new A values, because it doesn't know how. The types tell you a great deal before you've read any code.

A library of generic helpers

Code Block
TypeScript 5.7

Nothing in these functions cares whether A is a number, a string, or a complex object. That's parametric polymorphism. The compiler guarantees they all work for every A.

Ad-hoc polymorphism: "different behavior per type"

Sometimes a function does need to know something extra about its type — for example, "I know how to compare two values for equality". In Haskell that's called a type class:

class Eq a where
  eq :: a -> a -> Bool

TypeScript doesn't have type classes, but you can simulate them with one of the simplest tricks in FP: a type class is a record of functions. You pass the record as an argument; the generic function uses it.

Eq as a record

Code Block
TypeScript 5.7

The pattern is profound: an instance of a type class is just a value. You can:

  • have multiple instances for the same type (case-sensitive vs case-insensitive Eq<string>),
  • derive instances from other instances (eqArray from any Eq<A>),
  • pass instances around explicitly, log them, swap them in tests.

In Haskell the instance is chosen automatically by the compiler. In TypeScript it's chosen by the caller. Same idea, slightly more ceremony, more flexibility.

Ord as a record

A more useful example: ordering, with Ord derived to compare tuples lexicographically.

Code Block
TypeScript 5.7

ordPair and reverseOrd are combinators on instances. Instead of writing one specific comparator per task, you build sort orderings algebraically.

Type-class hierarchies (briefly)

Some classes extend others. The classical hierarchy from FP looks like:

The arrows mean "extends" — every Ord instance is also an Eq (it can decide equality from "neither less nor greater"). Every Monad is an Applicative, etc.

We'll meet Functor, Applicative, and Monad in the next chapter as patterns rather than required code structure. For now, what matters is: typical FP abstractions are just bundles of operations, and TypeScript can model them as records of functions plus generic functions that consume those records.

Why bother?

A Set<string> that uses case-sensitive equality is, in a real sense, a different data structure from one that uses case-insensitive equality — even though the values are the same. Type classes let you write one set implementation that works with either, by parameterizing it on Eq<string>.

The same pattern shows up everywhere:

  • a JSON encoder parameterized by Show<A>,
  • a hash table parameterized by Hash<A>,
  • a sort parameterized by Ord<A>,
  • a merge parameterized by Semigroup<A> ("how do I combine two A values?"),
  • a sum parameterized by Monoid<A> ("how do I combine, and what's the empty?").

You write the algorithm once; you pass the behavior in.

A Semigroup challenge

A Semigroup is the simplest possible algebraic abstraction: an associative binary operation. Numbers under addition, strings under concatenation, arrays under concatenation, and "last wins" on objects are all semigroups.

Challenge
TypeScript 5.7
Build a generic 'reduce' from a Semigroup

In semigroup.ts, implement combineAll(s, xs) that combines a non-empty list of A values using a Semigroup<A> (one operation: concat(x, y) => A).

For an empty list, return undefined.

The provided instances are:

  • sumSG — numbers under +
  • prodSG — numbers under *
  • stringSG — strings under +

Expected output

10
24
abc
undefined

QuestionSelect one

What does it mean for a TypeScript function to be parametrically polymorphic over type A?

It accepts any argument and uses typeof to dispatch

It must work for some specific union of concrete types

It works uniformly for every choice of A and makes no assumptions about A's structure

It uses unknown everywhere and casts at the boundary

On this page