VOOZH about

URL: https://blog.logrocket.com/put-the-typescript-enums-and-booleans-away/

⇱ Put the TypeScript enums and Booleans away - LogRocket Blog


2020-12-14
1057
#typescript
Paul Cowan
30575
πŸ‘ Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

One of the first things I liked about the brave new world of TypeScript was the TypeScript enum. I had previously used them in C# and felt a reassuring familiarity.

πŸ‘ Put the TypeScript enums and booleans away

Enums are a set of named constants that can take either a numeric or string form. I have always used the string-based enum, which I will use for all the examples in this post:

enum State {
 on = 'ON',
 off = 'OFF,
};

πŸš€ Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

Inappropriate use

I used enums in all sorts of inappropriate places, such as the string type in Redux actions before @reduxjs/toolkit helped alleviate the notorious Redux boilerplate:

enum AuthActionTypes {
 SetForcePasswordChange = "SET_PASSWORD_CHANGE"
}

interface ForcePasswordChange {
type: AuthActionTypes.SetForcePasswordChange;
}

export const forcePasswordChange = (): ForcePasswordChange => ({
 type: AuthActionTypes.SetForcePasswordChange
});

My motivation was to avoid annoying string typo errors, and for this requirement, it works.

Armed with a new hammer everything looks like a nail

I became more adventurous as my confidence grew with this new alluring Frankenstein construct that exists both at build time and at run time.

Enums seemed an excellent choice for modeling states in a finite state machine.

What I did not know at the time was that I was missing one of TypeScript’s most outstanding features that goes a lot further than just making sure I have a mutually exclusive set of constants:

enum AuthenticationStates {
 unauthorised = "UNAUTHORISED",
 authenticating = "AUTHENTICATING",
 authenticated = "AUTHENTICATED",
 errored = "ERRORED",
 forcePasswordChange = "FORCE_PASSWORD_CHANGE"
}

In the above example, I have an AuthenticationStates enum that models an authentication workflow.

A user starts in an UNAUTHENTICATED state before transitioning to AUTHENTICATING, etc. My carefully crafted enum ensures that the user cannot be in more than one contradicting state at any one time. For example, they cannot be AUTHENTICATED and AUTHENTICATING.

I need more juice

I then realized that I would need additional data, for example, if an error occurred then I would need to know what the actual Error object was.

I initially modeled the new requirements like this:

enum AuthenticationStates {
 unauthorised = "UNAUTHORISED",
 authenticating = "AUTHENTICATING",
 authenticated = "AUTHENTICATED",
 errored = "ERRORED",
}

type State = {
 current: AuthenticationStates;
 isLoading: boolean;
 authToken?: string;
 error?: Error;
};

const current: State = {
 kind: AuthenticationStates.authenticated,
 isLoading: false,
 authToken: 'token',
 error: undefined
};

I diligently ensured that each authentication state could have the same type of fields.

The problem with this approach is that each state has the same fields and could be a great source of bugs as I might get lazy and start copying and pasting.

I then discovered discriminated unions that are also known as algebraic data types.


Over 200k developers use LogRocket to create better digital experiences

πŸ‘ Image
Learn more β†’

Discriminated unions a.k.a algebraic data types

If you want to impress people at a party, then telling them that you use algebraic data types daily is a guaranteed home run!

In TypeScript we can create a string union that is very similar to our Authentication enum:

type AuthenticationStates =
 | "UNAUTHORISED"
 | "AUTHENTICATING"
 | "AUTHENTICATED"
 | "ERRORED";

I can use this as a more powerful string parameter type that gives better guarantees about what values are allowed.

Unions in TypeScript can be unions of many things and not just primitive types. We can make the world a better place by creating a union that only needs to have the same kind field as each element in the union. The kind field will act as the discriminator:

export type AuthenticationStates =
 | {
 kind: "UNAUTHORISED";
 context: {
 isLoading: false
 };
 }
 | {
 kind: "AUTHENTICATING";
 context: {
 isLoading: true;
 };
 }
 | {
 kind: "AUTHENTICATED";
 context: {
 isLoading: false;
 authToken: string;
 };
 }
 | {
 kind: "ERRORED";
 context: { isLoading: false; error: Error };
 };

The above type is both beautiful executable documentation, and we can, at a glance, see all the available states in the workflow.

The discriminator in the above example is the kind field that the compiler uses to type narrow or apply more specific rules as it determines which exact element of the union a variable might be.

The critical takeaway here is that only the appropriate data is available on each type.

We have no business trying to access an authToken if we are not currently in the AUTHENTICATED state.

What is uber exciting is that the compiler can enforce this order of correctness better than a programmer who has spent too much time on the JVM can during a code review.

Type narrowing on a discriminated union

Below is an illustration of how TypeScript can type narrow on a discriminator of a union:

const transition = (state: AuthenticationStates) => {
 switch (state.kind) {
 case "UNAUTHORISED": {
 console.log(state.context.userName); // only available in UNAUTHORISED

 // this is hot!! the compiler will not allow us to access the authToken in this state
 console.log(state.context.authToken); // Property 'authToken' does not exist on type '{ isLoading: false; userName: string; password: string; }'
 break;
 }
 case "AUTHENTICATING":
 console.log(state.context.userName); // Property 'userName' does not exist on type '{ isLoading: true; }'.
 // Type 'false' is not assignable to type 'true'
 state.context.isLoading = false;
 // The only assignable value is true in this state
 state.context.isLoading = true;
 break;
 case "AUTHENTICATED":
 // here and only here do we have an authToken
 console.log(state.context.authToken);
 break;
 case "ERRORED":
 console.log(state.context.error);
 break;
 }
};

You can also check out the CodeSandbox here.

We can only access specific data when the compiler has type narrowed by using the kind discriminator field.

The best example is this:

πŸ‘ unauthorised message

The compiler will error if we try and use the authToken in the wrong state.

Booleans do not model state

Any attempts to model state with Booleans will fail in an explosion of contradicting variables and a stressed developer.

For example, if we tried this approach:

const isAuthenticated: boolean = false;
const isErrored: boolean = false;
const isLoading: boolean = false;

It will not take long before we start combining these suckers into a mess of tangled logic that just keeps on spinning.

if (isErrored &&. isAuthenticated === false) {
 // do this
} else if (isLoading && is isErrored) {
 // do something else

Where is the algebra?

These fancy-sounding algebraic data types are nothing more than a way of saying that a type is composed of other types. That is it. Not that fancy at all.

Epilogue

Unions serve as great-looking executable documentation and also keep the errant programmer on the straight and narrow which is especially prevalent in the Wild West landscape of what used to be JavaScript programming.

Algebraic data types have existed in functional languages such as Haskell for some time, and it is exciting that TypeScript has brought them to the great unwashed of frontend developers.

LogRocket understands everything users do in your web and mobile apps.

πŸ‘ LogRocket Dashboard Free Trial Banner

LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings β€” compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.

With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.

Modernize how you understand your web and mobile apps β€” start monitoring for free.

πŸ‘ Image
πŸ‘ Image
πŸ‘ Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

Debug Next.js apps with AI agents and next-browser

Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.

πŸ‘ Image
Emmanuel John
Jun 17, 2026 β‹… 9 min read

Stop hardcoding LLM SDKs: Dynamic LLM routing with OpenRouter and Next.js

Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.

πŸ‘ Image
Chizaram Ken
Jun 16, 2026 β‹… 13 min read

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension β€” no new framework required.

πŸ‘ Image
Ikeh Akinyemi
Jun 12, 2026 β‹… 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo β€” with email/password login, Google OAuth, session persistence, and protected routes.

πŸ‘ Image
Chinwike Maduabuchi
Jun 9, 2026 β‹… 13 min read
View all posts

Hey there, want to help make our blog better?

Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now