Intro to string literal types
Tags: typescript
Published: 2020-05-16 18:55:11

Are you using [string literal types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types)? They're by far my most favorite feature in TypeScript. In their simplest form, they're used as a set of self-documenting props. In advanced cases, [exhaustiveness type checking](https://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checking) gives you confidence knowing that you'll receive compile errors when making updates without properly handling all cases.

If you're new to TypeScript and not yet using string literal types, then let's go over three reasons why you'll want to start using them:
1. [Self documenting props](#self-documenting-props)
2. [Pairing with Record\<K, T\>](#pairing-with-recordltk-tgt)
3. [Exhaustiveness checking](#exhaustiveness-checking)

# Self documenting props
I need a helper function that returns a number when given input of small, medium, or large. This is a pretty common pattern when creating React components and could be used for something as simple as supporting different sizes of buttons. How would I do this without string literal types?

*Note: This is probably the worst possible way to handle this. Don't do this!* 🤠
```typescript 
const getPx = (size: string): number => {
  if (size === "large") {
    return 20
  } else if (size === "medium") {
    return 16;
  } else {
    return 12;
  }
};
```

From looking at this, it's pretty clear what props are supported, but I need to look at the code to understand it. What if this is moved to a library? Would I need to go to the readme or to the source code to remind myself which props are supported? This is a quick way to guarantee that nobody uses this helper function.

Converting this to use a string literal type does two things:
1. Defines a clear set of accepted properties. If you're using an IDE with IntelliSense then you'll see supported props as you type.
2. Compile time error when using unsupported props:
```typescript
type Sizes = "small" | "medium" | "large";
const getPx = (size: Sizes): number => {
  if (size === "large") {
    return 20
  } else if (size === "medium") {
    return 16;
  } else {
    return 12;
  }
};

getPx("xlarge")
// Argument of type '"xlarge"' is not assignable to parameter of type 'Sizes'
```
-----
# Pairing with Record\<K, T\>
Pairing string literal types with the [Record\<K, T\>](https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkt) utility type is a slick pattern to use when you want to cleanup your code.

Let's apply this pattern to `getPx`:
```typescript
type Sizes = "small" | "medium" | "large";
const sizes: Record<Sizes, number> = {
  small: 12,
  medium: 16,
  large: 20
}
const getPx = (size: Sizes): number => {
 return sizes[size]
};
```

That's a lot cleaner! Not only is it easier to add new sizes, but you'll also receive an error if you update `Sizes` without updating the `sizes` object.
```typescript
// Add a new size without updating `sizes`
type Sizes = "small" | "medium" | "large" | "xlarge";
// Property 'xlarge' is missing in type '{ small: number; medium: number; large: number; }' but required in type 'Record<Sizes, number>'
```
-----
# Exhaustiveness checking
Another lesser known feature of string literal types is that you can benefit from what's known as [exhaustiveness type checking](https://www.typescriptlang.org/docs/handbook/advanced-types.html#exhaustiveness-checking) when using switch statements. This means that TypeScript follows the flow of your code to determine if all cases are covered. If your code is too complex to work with the Record\<K, T\> pattern, then using exhaustiveness checking is a way to remove brittleness from your code.

This can be demonstrated most easily by converting [this reducer](https://reactjs.org/docs/hooks-reference.html#usereducer) to TypeScript
```typescript
type ActionType = "decrement" | "increment";
type Action = { type: ActionType };
type State = { count: number };
const initialState: State = { count: 0 };

function reducer(state: State, action: Action): State  {
  switch (action.type) {
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
  }
}
```

The key part of making this work is to remove the `default` case from the switch statement. By removing the `default` case, you'll run into a compile error when updating `ActionType` without updating the switch statement to handle the new case:
```typescript
// Add "reset" to `ActionType'
type ActionType = "decrement" | "increment" | "reset";
// Function lacks ending return statement and return type does not include 'undefined'
```
-----
# Conclusion
String literal types solve a handful of problems that help developers write clean, maintainable, and scalable code.