Danny's Blog
See all posts

Types Are Not Great Documentation

Written by: Danny Spencer

Updated November 11, 2022

Understanding the problem is more important

A huge obstacle in software development is understanding the thing in the first place. In fact, I'd argue it's one of the biggest obstacles. I suspect this is a leading reason why static typing is advocated for. But I want to put the focus back on understanding, because that takes precedence over tech stacks, langauges, and type systems.

In my experience, there are two layers to undertanding software:

The most important part of any project is always conceptual. It's also the hardest to get right. Why do we need this project in the first place? What are the key architectural components?

The implementation is the easy part. It takes time, but it's easy (comparatively).

E.g The Linux Kernel:

Types aren't that great for documentation. Examples are!

Try reading auto-generated commentless JavaDocs someday, or a man-page without examples. It lists what's there - the arguments, flags, methods, etc...

… but what does that mean semantically? Why would I call this? How am I supposed to call this?

The biggest advocates for static type systems will insist that types are self-documenting. I humbly and strongly disagree. In fact, some of the worst documentation I've seen have come from such projects, and I suspect it comes from a delusion that you don't need to document anymore.

Very Busy Developers will insist that JavaDocs are documentation. When developers lean on auto-generated docs to close the Documentation task on their scrum board, it discourages cross-sectional documentation by example.

I learn best by example - and I know I'm not alone. After all, all abstractions are built on top of concretions (examples).

Let's take Ramda's R.path documentation without the example:

[Idx] → {a} → a | Undefined
Idx = String | Int

Can you guess what it does?

The Ramda docs use a Haskell notation. I'll also rewrite it to a more familiar TypeScript-style notation:

path (l: array of Idx, m: map of (Idx,A)) -> A or undefined

We know that path is a function that takes two arguments, and returns the generic type A (or undefined).

Here's the example:

R.path(['a', 'b'], { a: { b: 2 } });    // returns: 2
R.path(['a', 'b'], { c: { b: 2 } });    // returns: undefined

You can probably figure out what this does by looking at the examples. If you do have additional questions about how this works, you can always fire up a Node.js REPL and try it out yourself.

In conclusion, a plea: Add more examples!

I'm begging documentation writers everywhere to understand: It's not enough to just tell us what's there. You need to tell us when/why to use it, and how to use it in a practical sense.