TypeScript is best simple

This post might be naive, but like many bloggers, I write to clarify my assumptions and to receive feedback when I’m wrong. The thought I’d like to explore is that TypeScript is best when its types are defined simply and we allow the simple type declarations to inform our design decisions. To be clear, I mean that code that is sometimes less flexible and less reusable is often better than sacrificing the simplicity associated with clear and easy to understand type declarations.

TypeScript Should Change how you Write JavaScript
When you start TypeScript, it is generally better to change the way you write JavaScript rather than trying to make TypeScript apply to your existing patterns. Without type definitions, it can be easy to make variables mean different things. This is often reflected in JavaScript variables, function parameters, and function return types. I’ll demonstrate this with a couple of JavaScript anti-patterns as examples. Using TypeScript as a guide, the anti-patterns are resolved.

Example JS Anti-Pattern #1 Over accommodating functions

function sum (a,b) {// double equal to also check for the undefined case
if (a == null || b == null) {
return NaN;
}
//coerce variables to number in case its a string and add
return +a + +b;
}

Let’s just change the first line to TypeScript where we defined the function and its parameters without adjusting the implementation.

function sum(a?: number | string, b?: number | string): number

What is hopefully clear at this point is that one of the great benefits offered by TypeScript in this scenario is that it allows us to simplify the implementation and types. Under the limitations of JavaScript, it’s 100% acceptable to try to accommodate the unpredictably of JavaScript, but with TypeScript, there’s no reason to assume that a sum function should for some strange reason take a string or might have undefined variables. The function really should be as simple as this:

const sum = (a: number, b: number): number => a + b;

A common argument against TypeScript is that it’s less flexible, so does this function become less flexible? It does, but it becomes easier to think about it and if there’s some stringified or undefined number in our application, its not the job of our sum function to deal with that odd case.

Example JS Anti-Pattern #2 Unnecessary multiple return types — AKA functions that do more than one thing

The single responsibility principle is certainly applicable to functions and simpler TypeScript interfaces help us enforce this. Take this JS example:

// JS
function queryUsers(username) {
if (username === '*') {
return users;
}
return users.find(u => u.username === username);
}
// TS
function queryUsers(username: string): User | undefined | User[] {
if (username === '*') {
return users;
}
return users.find(u => u.username === username);
}

This example is simple to demonstrate the point, but in real life, it often (shouldn’t but unfortunately) looks like this:

function queryUsers(username) {
if (username === '*') {
// INSERT A BUNCH OF RULES, SIDE EFFECTS, AND LOGIC HERE
return users;
}
// INSERT A BUNCH MORE RULES, SIDE EFFECTS, AND LOGIC HERE
return users.find(u => u.username === username);
}

Even without the anticipated complexity of production apps, would it not be simpler to follow if we stuck with a single User return value in a queryUsers function alongside a completely separate getAllUsers function that had no parameters which returned a list of Users as shown in the code below?

function getAllUsers(): User[] {
return users;
}
function queryUsers(username: string): User | undefined {
return users.find(u => u.username === username);
}

Generics are good
Code re-usability done well is a sign of well designed code. When I talk about hard to understand TypeScript, I want to be clear that I’m not speaking to generics which are generally straight forward to understand. For example, as far as I can tell, there wouldn’t be anything wrong with creating some utility function that extends objects that contain an ID to accomplish some task. My example will be over simplistic (and you’d probably just inline this rather than making it reusable), but it gets the point across:

function getItemById<T extends{id: string}>(items: T[], id: string): T | undefined {
return items.find((i) => i.id === id)
}

A really good question we have to ask ourselves at this point is: “If it’s generally better for a variable to have less types, how can generics be good? Don’t they imply potentially a bunch of types for the same variable?”

The difference is that well designed functions with generics are generally more about allowing structures that contain variables with types that we don’t care about. In our example above, getItemById can take any object that has an id with type string. The generic doesn’t mean that we have to handle additional cases for the other variables that might exist in the object, rather it specifies that we really only care about one which is the id.

Conclusion
I could be totally wrong or misthinking some of this. There’s cases where we see really complicated TypeScript interfaces in libraries or code bases that went back and hacked in TypeScript later which is totally fine — The task was probably to add types to existing patterns rather than create new ones. In code that you write for the first time with TypeScript, it just seems that you’re better off if you use simple interfaces rather than complex.

A final thought — A function with a complex interface is almost necessarily difficult to understand. This alone is sufficient for me to question the necessity of complicated type declarations. Some things are just complicated, but with most variables and functions, a skillful developer can break complicated things into its simplest pieces.