As part of the communication exercise, you should write code as if you’re writing in your ideal language. When the language does not have a construct you want to use, it is still incredibly helpful to communicate intent to the reader by leaving a structured comment. For example, an early version of TypeScript didn’t have support for protected
members in a class, so I used to write
public /* protected */ memberFunction(): string {
}
Once support for protected
was added, it was a simple global search-and-replace exercise to capitalize on the new feature. But even if it was never added to the laguage, the clear declaration of intent makes it worth your while.
Similarly, you can use type aliases to declare intent. For example,
type Year = number;
type UserId = number;
type Count = number;
allows you to then write
function getMealCountByYear(userId: UserId): Map<Year, Count>;
Each of these types is a communication tool, they do not in this implementation provide type safety[1]. Count
could have also been something like UnsignedInt
.
Another example is named parameters. I personally don’t care much for reordering parameters, as it goes against my strong preference for consistency, but it does allow to give context to otherwise unlabeled values.
loadData(user, true)
This is pretty unclear. Sure, you can get parameter info with a few clicks in a good editor, but that’s extra brain cycles spend pointlessly. And if you’re using some web interface to do a code review for a peer, or are looking at a diff, or browsing the repo in some other interface, you don’t get context. Instead, you can easily emulate named parameters (in a language that doesn't have native support for them) with
loadData(user, /* shouldBustCache */ true)
Another example is something we did about the problem (coming from the Scala world) that if
s in JS are not expressions. In Scala you can say (contrived)
val mealFuture = isInCache(mealId) match {
case true => fromCache(mealId)
case false => fromDB(mealId)
}
so in our TypeScript codebase, we added a library function to do something similar, and adopted a standard structured comment for parameters
const mealPromise = ifMatch(isInCache(mealId),
/* if */ () => fromCache(mealId),
/* else */ () => fromBackend(mealId)
);
Chances are there are many useful programming constructs that are not available in the run-of-the-mill languages, so it may be worth your while to expose yourself to esoteric languages with powerful, expressive features. You may not end up writing production code in these languages, but they will enrich your toolbox nonetheless, and you can imitate their use in run of the mill programming languages.
[1] In TypeScript, for values that originate in either the backend or a very limited set of locations in the code, you can use type guards to add some safety to aliases.
interface UserId {
__type__UserId__: void;
}
This works well for id
fields, since they are usually only generated on the backend, so once you’ve written the typings for the wire payloads, no casts are required anywhere in your code, and for any function that takes a UserId
as a parameter, you get type safety, so you can never get screwed by switching the order of parameters in calling
function doSomething(userId: number, mealId: number)
because each id has an assignment-incompatible type. For the case where ids are generated inside the typescript code base, either a cast or a type parametrized function does the trick.
Comments
Post a Comment