Introduction
Typescript namespaces create about as much drama as a two-party senate system. Over at typescript-eslint, the anti-namespace lobby has pushed through a resolution banning namespaces from the typescript-eslint defaults, known as the no-namespace rule. The justification for the default is reasonable, and is further corroborated by the warning you are faced with when visiting the official namespaces page for Typescript. Essentially, Typescript users are being told that namespace
is an outdated module system syntax that needs to be abandoned. I agree, nominally and naively, with this claim. But, I don't think devs should full-sail abandon the namespace
keyword within their codebases. There is another, lesser known, but arguably more important utility for the namespace
keyword: Namespaces enforce logical groupings.
I believe that logical grouping is a vastly underutilized utility of pretty much every programming language. I think this plays a large part in the universally acknowledged hard problem of naming things in programs. I believe Typescript developers are fortunate that the Typescript language has a keyword that (in modern Typescript) is solely used for logical grouping.
Outline
In this piece, I will briefly introduce a collection of keywords in Typescript, enum
, interface
, class
, and namespace
(and type
, sort of). I will then introduce a strict variant of the DRY (Don't Repeat Yourself) Principle. Finally, I will combine everything together, which will hopefully demonstrate the strong type safety and code cleanliness that Typescript can achieve.
Enums
An enum is a glorified number line. It is short for "enumeration", and is useful when enumerating a custom list of just about anything.
enum Color {
Blue,
Green,
Red,
}
Typescript assigns numbers to each enum value. Color.Blue === 0
, for example. However, you can override the default behavior and assign enum values to strings:
enum Color {
Blue = "blue",
Green = "green",
Red = "red",
}
Now, Color.Blue === 'blue'
. This is the way we will use them in the final section.
Interfaces
An interface specifies how an object behaves, for example, by dictating what keys and values are present on an object. The compiler can use that specification to ensure your objects are structured appropriately. You are basically letting the compiler double check your work. (I think of interfaces as a kind of unit test.)
interface Pizza {
toppings: string[];
crust: string;
size: string;
slices: number;
extraCheese: boolean;
}
Using the Pizza
interface above means that any pizza you make, const pizza: Pizza = ...
, will have to conform to the interface, otherwise the compiler will error.
An Interface including Enums
Enums and interfaces can be combined together in a powerful way. I want to demonstrate this by pointing out an issue in the above Pizza
interface, and then show how using an enum can help resolve it.
A problem with the Pizza
interface above is that the crust
and size
keys are merely strings. That means I create a pizza with an absurd size
value and my code won't notice that anything is wrong. pizza.size = 'blog post'
is fine from the compiler's perspective.
We don't have to live this way. We can create an enum, enumerating all the size
options, and enforce that size
is only one of those enum options.
enum Size {
Small,
Medium,
Large
}
interface Pizza {
...
size: Size,
...
}
In your code, you now must assign a pizza's size
value to a Size
enum value, pizza.crust = Crust.Pan
. Anything else will throw a compiler error.
If you haven't noticed the power of this maneuver just yet, please note that you have just constrained your Pizza
interface down to exactly 3 options, as opposed to previously when any of the countably infinite string values were allowed. From countably infinite to 3 is a substantial downsizing.
Classes
Classes in Typescript can be utilized in a variety of ways. One creative way of using classes is by using them as a mere data store by leveraging the static properties that you can assign to classes. Simply put, it's totally legit to write classes like this,
class PizzaSize {
static Small = "small";
static Medium = "medium";
static Large = "large";
}
This looks almost exactly like the enum
above. Also, we could create an interface
and a type
that look a lot like this as well. There are important differences between all four options in Typescript. One way to think about the differences is that classes and enums reference primitive values, like strings and numbers, while interfaces and types specify behavior. The former are used within functions, the latter are used to constrain functions. So, use classes and enums if you want to reference a string, number, etc., and use interfaces and types if you want to constrain possible behavior.
Choosing between Classes and Enums
We now know that classes and enums can both be used as primitive data stores. So, which do you use? For example, if you wanted to enumerate pizza sizes and reference those sizes in string form within your codebase, you could do either of the following.
class Size {
static Small = "small";
static Medium = "medium";
static Large = "large";
}
Or,
enum Size {
Small = 'small'
Medium = 'medium'
Large = 'large'
}
My recommendation is to use enums until you have a reason not to. There are plenty of constrained-choice lists in codebases, and enums are the perfect tool for the job. However, if you need more complexity, don't fear swapping it out for a class object. For example, let's say that, in addition to storing string values for the options, i.e., Size.Small === 'small'
, we also want to store a string value for the group of options, i.e., something like Size === 'pizza sizes'
. An enum wouldn't work here. The naive workaround is keeping the enum and adding an associated constant, for example,
const SizeString = 'pizza sizes'
enum Size {
Small = 'small'
Medium = 'medium'
Large = 'large'
}
However, now you need to keep track of the name of the const and the name of the enum. You've also doubled the amount of names related to pizza sizing. In this contrived example, that might not seem like a big deal, but in a large codebase, that doubling can turn 10 named things into 20 named things, which also has the knock-on effect of increasing the likelihood of name collisions in your codebase.
An easier option would be to swap out the enum for a class and change the default value of the toString
method.
class Size {
static toString = () => "pizza sizes";
static Small = "small";
static Medium = "medium";
static Large = "large";
}
This solves the problems of name explosion and name collision, and also preserves the logical grouping. It's worth noting that toString
is a function, and not a variable. Also, it's a special function that is automatically called when you try to "print" the class. That means it is automatically invoked when string-interpolated, e.g., `${Size}` === 'pizza sizes'
.
We will use the above trick in the final section.
Namespaces
As mentioned in the introduction, namespaces enforce logical groupings. For example, I can create a pizza namespace that logically groups all the objects we've created that are related to pizza.
namespace Pizza {
export class Size {
static toString = () => 'pizza sizes'
static Small = 'small'
static Medium = 'medium'
static Large = 'large
}
export enum Crust {
Pan,
Regular,
Thin
}
export enum Slices {
Two = 2,
Four = 4,
Six = 6,
Eight = 8
}
export interface IPizza {
toppings: string[],
crust: Pizza.Crust,
size: Pizza.Size,
slices: Pizza.Slices,
extraCheese: boolean
}
}
Anything I do not export
from the namespace can be used internally, for example, a constant value that is shared across a few different exported objects can be stored as a non-exported const within the namespace. You can store non-exported functions and classes in namespaces as well.
In Defense of Namespaces
In the past, when I have not used namespaces, I've found myself with constants like pizzaSize
, garlicBreadSize
, sodaSize
, etc., all of which began their lives as a const named size
, which I eventually discovered created a name collision in my codebase. It would take a long time to debug. Then, I'd have a long, hard think about my codebase's naming conventions, change some things around, add words to my constant names, try to commit, realize I forgot a few renames that my IDE didn't catch, try to commit again, realize I accidentally created another name collision, fix that one too, try to commit again, and then finally succeed, feeling aesthetically nonplussed.
As I mentioned above, it's no secret that naming things is hard to do in a codebase. One of the reasons this is so difficult is because logical-grouping functionality is underutilized. Well, in Typescript, it shouldn't be. You no longer have to pollute the global namespace with fifteen names that, after name-collision resolution, will be prepended with their group name anyways. Instead you only have to add one name to the global namespace, inside of which is fifteen, smaller, cleaner names that make perfect sense within their logical group.
Your rebuttal might be: "Who cares if my const is pizzaSize
or Pizza.Size
?" The answer is no one, if you don't. If your system is copacetic with pizzaSize
, then don't change it. Do what works. But, as a system grows, refactoring into a logical grouping like Pizza.Size
will pay dividends. It's hard to remember 20 keywords. It's easy to remember that there's this one, well named keyword, inside of which is 20 other keywords.
DRY code includes DRY strings
DRY is an acronym for Don't Repeat Yourself. It is a well-regarded programming principle which, in my opinion, is pivotal to clean code. Similar functions doing slightly different things is the devil of consistent behavior.
I love the DRY principle. I might love it too much. You might find this a bit extreme, but I think that if I've typed the same word into more than one string, then I've repeated myself in an anti-DRY capacity. For example, let's say I built a website for a fictional pizza place called Pizza Palace. The landing page says "Welcome to Pizza Palace". The subtitle says "We have over 12 pizzas to choose from". A dropdown is titled "pizza options". Another, "pizza toppings". There are pages devoted to pictures of each pizza, at the url "pizzapalace.com/pizzas/[pizza-option]" When you complete your order, a popup says, "Your pizza will arrive in 20 minutes!" You get the picture. I think there is a lot of repetition in these strings. I have typed the word "pizza", hard-coded, into this website multiple times. The probability that I have not noticed a typo of the word 'pizza' somewhere is pretty high.
The straightforward solution to this type of problem is to write the hard-coded word "pizza" into your codebase just once, and then reference it over and over again.
const pizza = "pizza";
const title = `Welcome to ${pizza.toUpperCase()} Palace.`;
const toppings = `${pizza} toppings`;
const orderComplete = `Your ${pizza} will arrive in 20 minutes!`;
Even better would be a Dictionary
object with every english word in it. E.g.,
const title = `${Dict.Welcome} ${Dict.To} ${Dict.Pizza} ${Dict.Palace}`;
const toppings = `${Dict.Pizza} ${Dict.Toppings}`;
const orderComplete = `...`;
However, that's a bit overkill. That being said, the const pizza = 'pizza'
strategy could make sense in some contexts, especially if the string 'pizza' determines the outcome of multiple pieces of functionality, for example, in the resolution of dynamic routes or in a switch-case statement. In those scenarios, a typo can break your entire system.
Type dreams can come true
Combining everything above, you can create an air-tight (error-tight?), error-resistant, data structure that is almost impossible to break. Gone will be the days where you updated a key somewhere, and forgot to update it elsewhere, resulting in endless debugging. Am I over-promising? Yes. But not by too much. Is it earth-shattering? No, but I think it is a decent type-paradigm worth sharing.
Let me show you a satisfying code snippet. Assume you have a website, like mine over at reftable.com, with the following structure: /alphabet/arabic
shows a table, /alphabet/french
shows a table, coding/git
shows a cheat sheet, coding/python
shows a cheat sheet. Also, /alphabet
and /coding
are index pages that show a list of their sub-pages. Assume these pages are populated dynamically from a single, large data object that contains all the page information. This data object is powerful. You can create new pages by simply adding a key within it. It also consolidates all the information that would otherwise be strewn about across individual pages. However, this object has the potential to be scary to change, and making an edit in one place in the object, but not in another, could create a cascading chain of errors that's difficult to resolve. In brief, this large data object is powerful, but it's also type-scary, not type-safe. But with everything we've just learned in Typescript, we can easily transition from type-scary to type-safe. Check it out,
export namespace Page {
export enum Type {
Table,
CheatSheet
}
export class Alphabet {
static toString = () => 'alphabet'
static Arabic = 'arabic'
static French = 'french'
}
export class Coding {
static toString = () => 'coding'
static Git = 'git'
static Python = 'python'
}
}
export interface IPages {
[groupPageName: string]: {
pathname: string
pages: {
[pageName: string]: {
pageType: Page.Type,
page: ITablePage | ICheatSheetPage // interfaces not shown for brevity
}
}
}
}
const pages: IPages = {
[`${Page.Alphabet}`]: {
pathname: `/${Page.Alphabet}`,
pages: {
[Page.Alphabet.Arabic]: {
pageType: Page.Type.Table,
page: {
pathname: `/${Page.Alphabet}/${Page.Alphabet.Arabic}`,
...
}
},
[Page.Alphabet.French]: {
pageType: Page.Type.Table,
page: {
pathname: `/${Page.Alphabet}/${Page.Alphabet.French}`,
...
}
}
}
},
[`${Page.Coding}`]: {
pathname: `/${Page.Coding}`,
pages: {
[Page.Coding.Git]: {
pageType: Page.Type.CheatSheet,
page: {
pathname: `/${Page.Coding}/${Page.Coding.Git}`,
...
}
},
[Page.Coding.Python]: {
pageType: Page.Type.CheatSheet,
page: {
pathname: `/${Page.Coding}/${Page.Coding.Python}`,
...
}
}
}
},
}
The compiler knows the name of almost everything. The type safety is strong. The weakest link in the above code is the interface specifying key values as strings. Strings are open-ended types, so typing errant text within them will not be caught by the compiler. But, for everything else, any errant keystroke will fail to compile. That is powerful. That gives me a lot of confidence when I build React components to search the object and render the information. This structure gives me confidence to quickly change or update keys, rearrange the data structure, swap values, add features, etc.
Conclusion
I think one of the reasons I like this type-safe code so much is that it brings together a lot of the features I enjoy about Typescript and combines them in a way that showcases the power of the language. It feels like the sum of Typescript is greater than its parts.
Thank you for reading this, which I now realize is basically an ode to Typescript.
Have a good day :)