Minimal TypeScript

Although there's a lot of strong, perhaps negative opinions, on TypeScript in this piece, if you skim through the first paragraphs you'll see the point of it is to establish that TypeScript is, in fact, a valuable tool, and what a minimally useful feature set and usage pattern of TypeScript looks like to me.

I'm gonna try hard not repeat everything Eric Elliot and Andrea Giammarchi already said so eloquently in their pieces on the subject. There's a great deal of toxic positivity around TypeScript, that's for sure. Some Twitter blowhards and popular open source maintainers will champion it as revolutionary and indispensable, but that needs to be recognized for what it is: an opinion. People put it in a lot of time learning something and changing their way of doing things, so it's only natural they seek to strengthen their belief in their own choices by getting other people to... see the light, like they did.

My language is better than yours — that's always going to be a thing in our industry. No way around it. But when it comes to the merits of strong typing, it's a really a moot point. I witnessed this being discussed before, ad nauseum, when Python first rose to popularity. See, a lot of dynamically typed languages still power a lot of today's web: PHP, Python, maybe some Perl lurking in a dark corner.

Even TypeScript in all its glory relies on standard JavaScript to run in modern JavaScript engines. And Deno, well, Deno decided to remove TypeScript from its core a while ago. Cheap shots, for sure, but it gives me a chuckle when I see people tweeting about how undeniably great TypeScript is and that you should just stop whining and use it already. I mean, just look at that mountain of untyped JavaScript code, so heretic — how can you trust it?

Not a Silver Bullet

There's a lot of software I love that's built with TypeScript. Solid, Vue 3 and Vite are amazing, for instance. TypeScript, or at least having TypeScript declaration files, makes a lot of sense for open source libraries, because they can be used by hundreds of thousands of developers, and modern code editors are equipped to offer conveniences such as autocompletion, relying on it.

That doesn't mean all code needs to be written in in TypeScript in order to be excellent. Take Fastify — its source code has no TypeScript, just TypeScript declaration files. Fastify's source code reads like a module from the Node.js standard library. If you look through the source code for this bit of the streams module in Node.js, and this other file from Fastify's source code, you'll find the same idioms and techniques in structuring JavaScript code.

Fastify is written and maintained by a lot of people who are Node.js contributors themselves. It accounts for a lot for performance and safety traps when running JavaScript in Node.js, such as, ensuring effective use of the Event Loop, encapsulation and proper error handling in asynchronous code to prevent memory leaks. It also has comprehensive test suites.

Do you think Fastify lacks any quality because it is not written in TypeScript? Fastify has ranked pretty high in recent development surveys and has nearly 400k weekly downloads on npm. Hope these poor souls know what they're doing running extremely TypeScript deficient code on their servers.

TypeScript is not a silver bullet, nor is the belief that strong typing solves all your problems. It might make sense for some open sources libraries. It might solve or prevent a lot of problems. Problems that many proficient JavaScript developers solve with just good old fashioned testing practices and eslint.

Both eslint and tsc (--noEmit) can walk you through writing decent code and reduce the probability of errors significantly. It's a matter of depth. If you fill your code with type annotations everywhere, tsc will definitely catch way more instances of potential trouble, but not that many instances, that make you stop and think about how the hell you were doing your job before you started using it.

Avoiding Gratuitous Bureacracy

Despite all that, it has to be recognized TypeScript can be a valuable tool indeed. I love TypeScript declaration files and their ability to provide a quick yet precise snapshot of everything an API provides. I like it when I see an interface declaration to specify options that can be passed to a function via an object. But... that's... pretty much it. There's really not much else I like. I definitely don't like seeing every line of code with a type annotation mixed within.

TypeScript is rather flexible though. Let's start with the fact you can run regular JavaScript and tsc won't complain about it. Well, that's a relief.

function vanilla (something) {
  console.log(something)
}

vanilla(1)

If you add a string type annotation to something though:

export function vanilla (something: string) {
  console.log(something)
}

vanilla(1)

Now you get:

% npx tsc --noEmit index.ts
index.ts:8:9 - error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

8 vanilla(value)
          ~~~~~


Found 1 error.

That's great — but now, do you really want to clutter all of your JavaScript code with that kind of syntax? Is it really necessary everywhere? You might be tricked to think it's harmless and useful. I'll just add an extra little thing to every single variable and be done with it. But the committment required is what bothers me. The gratuitous bureacracy to coding, if you will, that is so unlike the extremely dynamic nature of JavaScript. The ability to deliver so much with so little code is one of the things that drove JavaScript to become a standard for the web.

Apologies for the obvious Paul Graham reference, but succinctiness is power. I'd rather be able to remain able to write massive amounts of code without first agreeing to add type annotations to everything. It's too high a commitment.

Do you actually need TypeScript at all? Does your codebase have more than a handful of people touching it? How will adopting TypeScript actually help with delivering and maintenance as opposed to just writing standard JavaScript and hardening it with a test suite? These are all questions to consider before jumping into making .ts the default extension for every JavaScript file you write.

A Kitchen Sink Example

Having said all that, this is what an acceptable use of TypeScript looks like to me: limited to primitive type annotations (string, number, boolean etc), arrays and interfaces specifying the shape of key objects, specifically those that are publicly exposed. Less is more. For those functions, you'll want to be specifying an interface for parameter options, and another for the result. The following example shows concisely the kind of syntax you need to learn to accomplish that:

interface Options {
  aString: string;
  aNumber: number;
  aBoolean: boolean;
  anOptionalBoolean?: boolean;
  anArrayOfStrings: string[];
}

interface Result {
  aString: string;
  aNumber: number;
  aBoolean: boolean;
}

export function foobar (options: Options): Result {
  return {
    aString: 'string',
    aNumber: 123,
    aBoolean: true,
  }
}

You can also of course have an array of other interface-specified objects, and even also specify that an object should have a few certain properties but it should also accept any other properties, like done below for InnerOptions:

interface Options {
  aString: string;
  aNumber: number;
  aBoolean: boolean;
  anOptionalBoolean?: boolean;
  anArrayOfStrings: string[];
  anArrayOfInnerOptions: Array<InnerOptions>;
}

interface InnerOptions {
  mustHaveThisOneString: string;
  [x: string | number | symbol]: unknown;
}

By just internalizing that syntax and key constructs, you're quite possibly already getting enough out of TypeScript in terms of automatic documentation and error prevention. If you call foobar() without any of the specified properties in the parameter object, like anArrayOfStrings:

foobar({
  aString: 'string',
  aNumber: 123,
  aBoolean: true,
  anArrayOfInnerOptions: [{
    mustHaveThisOneString: 'string',
    canHaveThisOtherThing: false,
  }],
})

This is what npx tsc --noEmit index.ts gives you:

This example shows what a minimal configuration for TypeScript projects can look like: you need .eslintrc extending standard-with-typescript and an empty tsconfig.json so you can reference it.

The following commands should work:

% npx eslint .
% npx tsc --noEmit index.ts

Wrapping Up

In this piece I promote the minimal adoption of TypeScript, on a per project basis, with the strict purpose of defining the shape of objects only where you can benefit from having it easily documented and it actually helps making the code easier to understand and reason about. Don't give in to gratuitous bureaucracy, don't try and account for every possible case. That's called validation, save it for ajv.

Want to hear about new articles on this site? Follow me on Twitter Twitter