I love . I’ve been using it for over 2 years in various projects, and the more I use it the less compelling I find vanilla Javascript. TypeScript Not that there is nothing wrong with vanilla Javascript ( is vanilla!), but I think that when it comes to medium to large projects, Typescript makes a lot of things easier. my blog Among the many good things Typescript offers, I’d like to address one that, in my experience, has saved me quite a few bugs. Let’s start with an example first. The code will contain React components, but the general principle stays the same with other frameworks as well. Let’s say we have a very rudimentary loading indicator in our app: import React ; RequestStatus = | | ; RequestLoadingIndicatorProps { state: RequestStatus; } const styles: Record<RequestStatus, React.CSSProperties> = { PENDING: { backgroundColor: , borderRadius: , width: , height: , }, FAILED: { backgroundColor: , borderRadius: , width: , height: , }, SUCCESSFUL: { backgroundColor: , borderRadius: , width: , height: , }, }; const RequestLoadingIndicator: React.FC<RequestLoadingIndicatorProps> = ({ state, }) => { return <div style={styles[state]} />; }; from "react" type "PENDING" "SUCCESSFUL" "FAILED" interface "blue" "50%" "50px" "50px" "red" "50%" "50px" "50px" "green" "50%" "50px" "50px" export Here's what it looks like: It’s nothing special, but our users are content. In order to display a loading indicator in our system all we need is to tell it in what state our request is, and it will display a circle in the corresponding color. One day, we choose to allow adding a message to go along with requests. We can modify our props interface like so: FAILED RequestLoadingIndicatorProps { state: RequestStatus; message: ; } interface string And our component will now display the message: RequestLoadingIndicator: React.FC<RequestLoadingIndicatorProps> = ({ state, message, }) => { ; }; export const return {message} < = > div style {styles[state]} </ > div A while passes and everything is just fine, but then - an engineer on our team is refactoring some old code, and rewrites some code to fetch data from your server. When the data arrives, the engineer renders a loading indicator with a message, although our guidelines specifically say that successful indicator should have a message. SUCCESSFUL not { { data } = useData(); (data) { ( ( ) function GetData const if return ); } } < = = /> RequestLoadingIndicator state "SUCCESSFUL" message "data fetched" Impossible State What we have here is an ! impossible state An impossible state is a certain combination of fields and values, that should never coexist simultaneously. In other words - an “impossible state” might be a in that if we disregard our company guidelines/lint rules/compiler, the state may occur, but we should never accept it, and therefore must make sure it never occurs (whether intentionally or unintentionally). possible state You don’t need Typescript to avoid impossible states. In fact - you get away without anything stopping you from making the impossible state mistake, given that everyone in your team is aware of it, and all of you are responsible engineers with buckets of ownership. could That might be the case . What will happen when your company doubles in size? or triples? or quadruples? today Would you still feel like word-of-mouth is good enough? I strongly disbelieve that. Not because I don’t trust other engineers around me, I have complete faith in them. I like to think about it in exponential terms - if your team doubled in size, you’d need 4 times the efforts to preserve code quality. To comply with that, we need some mechanism that would prevent, to the highest degree possible, the presence of such “impossible states”. Naïve solution One way to go about it, is to document the fact that or requests should have no message, like so: SUCCESSFUL PENDING RequestLoadingIndicatorProps { state: RequestStatus; message: ; } interface // Message should only be present when state is `FAILED` string But this method, in my opinion, is error prone - in the end the only way to find it is with a human eye, and humans are prone to failure. A better way But I am here to present to you a better way. There is a very simple way in which we can ensure we always have exactly what we want, nothing more and nothing less. We can leverage Typescript’s powerful . In essence, union types allow us to make new types that act as an clause in a way. Union Types OR Let’s start with a quick example. Say we have an intelligent logger that can both print single log messages, and can concatenate log messages if passed as an array. { ( .isArray(message)) { .log(messages.join( )); } ( messages === ) { .log(messages); } ( ); } log( ); log([ , ]); ( ) function log messages if Array console " " if typeof "string" console throw new Error "unsupported type!" "hello" // prints 'Hello'. "Hello" "World" // prints 'Hello World'. If we wanted to type it, we could do it naïvely like so: { ( .isArray(message)) { .log(messages.join( )); } ( messages === ) { .log(messages); } ( ); } log( ); log( ); ( ) function log messages: any if Array console " " if typeof "string" console throw new Error "unsupported type!" "Hello" // prints 'Hello'. 6 // this function will pass at compile time, but fail in runtime. But that won’t help us much, leaving us with pretty much untyped javascript. However, using union types we could type the function like this: { ( .isArray(message)) { .log(messages.join( )); } ( messages === ) { .log(messages); } ( ); } log( ); log([ , ]); log( ); ( ) function log messages: | [] string string if Array console " " if typeof "string" console throw new Error "unsupported type!" "Hello" // prints 'Hello'. "Hello" "World" // prints 'Hello World' 6 // Compile time error: Argument of type 'number' is not assignable to parameter of type 'string | string[]'. Now that we know how to work with union types, we can use them to our advantage in our loading indicator. One interface to rule them all? No Instead of using a single interface for all the possible states of the request, we can split them up, each having their own unique fields. PendingLoadingIndicatorProps { state: ; } SuccessfulLoadingIndicatorProps { state: ; } FailedLoadingIndicatorProps { state: ; message: ; } RequestLoadingIndicatorProps = PendingLoadingIndicatorProps | SuccessfulLoadingIndicatorProps | FailedLoadingIndicatorProps; interface "PENDING" interface "SUCCESSFUL" interface "FAILED" string type The highlighted part is where the magic happens. With it we specify all the different types of props we accept, and only allow a message on requests. FAILED You’ll immediately see that Typescript is yelling at our component: So we’ll change our component just a little: RequestLoadingIndicator: React.FC<RequestLoadingIndicatorProps> = ( props ) => { (props.state === ) { ; } export const if "FAILED" return {props.message} < = > div style {styles[props.state]} </ > div // no error! return ; }; < = /> div style {styles[props.state]} Inside our block Typescript is able to narrow down the type of our props from to , and ensures us that the prop exists. if PendingLoadingIndicatorProps | SuccessfulLoadingIndicatorProps | FailedLoadingIndicatorProps FailedLoadingIndicatorProps message If we now tried to render our with a message and a state other than , we would get compile time error: RequestLoadingIndicator FAILED Embracing difference We could stop at that and call it a day, or we can take it up a notch. What if we wanted to change our loading indicator to show an animation, and allow consumers of our indicator to pass a callback that fires when the animation ends? SUCCESSFUL With a monolithic interface, we’d go through the same trouble as we did when we added the field. message RequestLoadingIndicatorProps { state: RequestStatus; message: ; onAnimationEnd?: ; } interface // Message should only be present when state is `FAILED` string // onAnimationEnd should only be present when state is `SUCCESSFUL` => () void See how quickly it gets out of hand? Our union types make this a non-issue: PendingLoadingIndicatorProps { state: ; } SuccessfulLoadingIndicatorProps { state: ; onAnimationEnd?: ; } FailedLoadingIndicatorProps { state: ; message: ; } RequestLoadingIndicatorProps = PendingLoadingIndicatorProps | SuccessfulLoadingIndicatorProps | FailedLoadingIndicatorProps; interface "PENDING" interface "SUCCESSFUL" => () void interface "FAILED" string type Now, we only allow our indicator’s consumers to pass when state is , and we have Typescript to enforce that. onAnimationEnd SUCCESSFUL Notice that we used , so we don’t force anyone to pass empty functions. ? Summary Obviously, this is a contrived example, but I hope it makes it clear how we can leverage Typescript’s union types and type narrowing, ensuring as much type safety as possible, while still leveraging some of Javascript’s dynamic nature. Thank you for reading! Previously published at https://dorshinar.me/avoid-impossible-state-with-typescript