DEV Community

Danny Jones
Danny Jones

Posted on

Intro to string literal types

Are you using 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 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
  2. Pairing with Record<K, T>
  3. 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! 🤠

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:
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> utility type is a slick pattern to use when you want to cleanup your code.

Let's apply this pattern to getPx:

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.

// 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 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 to 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:

// 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.

Top comments (0)