The TypeScript Gluten Behind NgRx createActionGroup
Tags: typescript, angular, ngrx, opensource
Published: 2022-07-08 16:39:47

Creating strongly typed APIs in TypeScript relies on understanding advanced typing strategies. NgRx heavily depends on typing strategies such as string literal types, conditional types, and template literal types to create an API surface that encourages consumers to build strongly typed applications. Let’s take a look at an example API in the NgRx codebase to see how NgRx leverages these advanced typing techniques.

[NgRx v13.2](https://dev.to/ngrx/ngrx-action-group-creator-1deh) introduces a new function for defining groups of actions:

```tsx
const AuthApiActions = createActionGroup({
  source: 'Auth API',
  events: {
    'Login Success': props<{ user: UserModel }>(),
    'Login Failure': props<{ error: AuthError }>(),
  },
});
```

The type of `AuthApiActions` becomes an object containing methods for instantiating actions for each of the configured events:

```tsx
this.store.dispatch(AuthApiActions.loginFailure({ error }));
```

What excites me the most about this new API is that it is **100% type-safe**. How do we get from `‘Login Success’` to `loginSuccess` on the function names though? TypeScript’s type literal type manipulation!

## Going from ‘Event Name’ to eventName with TypeScript

NgRx Store’s codebase contains a utility type alias that converts `'Event Name'` to `eventName`:

```tsx
export type ActionName<EventName extends string> = Uncapitalize<
  RemoveSpaces<Titlecase<Lowercase<Trim<EventName>>>>
>;
```

`ActionName` is doing all of the heavy lifting to convert event names at the type level by:

1. Starting with a string literal type (`EventName extends string`)
2. Trimming it
3. Making it lowercase
4. Titlecasing each segment of the string
5. Removing the spaces between words
6. Lowercasing the first letter

There is a lot going on here, so let’s break it down step-by-step!

### 1. String Literal Types

My experience with advanced types in TypeScript is that advanced types are extremely relevant when writing libraries and not as relevant in application code. One core concept of advanced typing in TypeScript that library authors often take heavy advantage of is string literal types. If you haven’t encountered them before, a string literal type is a string type but narrowed down to a specific string.

This will be a little easier to explain with examples. Let’s say we have a function that takes the name of a kind of bread and prints it to the console:

```tsx
function bakeBread(kindOfBread: string) {
  console.log(`🥖 Baking: ${kindOfBread}`);
}
```

There’s a problem with this function. I can pass this function any string and TypeScript won’t care:

```tsx
bakeBread('Pie');
```

String literal types let us specify a concrete subtype of string to enforce correctness. For example, if we wanted to limit the type of `kindOfBread` to `"Wheat"` we could do this:

```tsx
function bakeBread(kindOfBread: 'Wheat') {
  console.log(`🥖 Baking: ${kindOfBread}`;
}
```

Now if we try to pass in a string that is not a kind of bread we get a type error:

```tsx
bakeBread('Cake');
```

This produces the error:

```
Argument of type '"Cake"' is not assignable to parameter of type '"Wheat"'.(2345)
```

Obviously, there are more types of cake than just `"Wheat"` though. By creating a union type of string literals, we can constrain the type of `kindOfBread` to be the kinds of bread that our program is aware of:

```tsx
type KindsOfBread =
  | 'Wheat'
  | 'White'
  | 'Rye'
  | 'Pumperknickel'
  | 'Sourdough'
  | 'Banana';

function bakeBread(kindOfBread: KindsOfBread) {
  console.log(`🥖 Baking: ${kindOfBread}`;
}
```

Now we can call `bakeBread` with a variety of valid bread types without error:

```tsx
bakeBread('Rye');
bakeBread('Sourdough');
bakeBread('Banana');
```

And if we try to pass in a kind of bread that our program is not aware of we get a type error:

```tsx
bakeBread('Pound Cake');
```

This results in:

```
Argument of type '"Pound Cake"' is not assignable to parameter of type 'KindsOfBread'.(2345)
```

### 2. Trimming String Literal Types

NgRx’s `ActionName` operates on string literal types. From here, it starts applying advanced typing on string literal types to coerce a string literal type of `"Event Name"` into `"eventName"`.

The first step is to trim the string literal types, or, in other words, remove any surrounding whitespace. That way, if the developer passes in a string like `"   Event Name"` we don’t produce a function whose name is   `eventName`.

To strip the whitespace around a string literal type, we are going to have to use **conditional types**. A conditional type is a type that checks if a condition is true or not at the *type level* and can conditionally return a different type as a result of the condition.

Let’s take a look at example code!

```tsx
interface SliceOfBread {
  toast(): void;
}

interface SliceOfCake {
  frost(): void;
}

interface Bread {
  slice(): SliceOfBread;
}

interface Cake {
  slice(): SliceOfCake;
}
```

In this example, our program has interfaces for `Cake` and `Bread` both of which have a `slice()` method for producing `SliceOfCake` and `SliceOfBread` respectively.

Now let’s write a function called `slice` that takes an object of type `Cake` or `Bread` and returns the right result:

```tsx
function slice(cakeOrBread: Cake | Bread): ??? {
  return cakeOrBread.slice();
}
```

What type should we use for the return type of this function? Naively, we could use `SliceOfCake | SliceOfBread`:

```tsx
function slice(cakeOrBread: Cake | Bread): SliceOfCake | SliceOfBread {
  return cakeOrBread.slice();
}
```

This would require the consumer of `slice` to inspect the return type to know if it got back a slice of cake or a slice of bread. For example, if we tried to toast a slice of bread we get back when passing in pumperknickel:

```tsx
slice(pumperknickel).toast();
```

We get an error back from the TypeScript compiler:

```
Property 'toast' does not exist on type 'SliceOfCake | SliceOfBread'.
  Property 'toast' does not exist on type 'SliceOfCake'.(2339)
```

We could use function overloads to write `slice` in a way that works correctly:

```tsx
function slice(cake: Cake): SliceOfCake;
function slice(bread: Bread): SliceOfBread;
function slice(cakeOrBread: Cake | Bread): SliceOfCake | SliceOfBread {
  return cakeOrBread.slice();
}
```

This removes the type errors and all of the types are inferred correctly. However, we can shorten this by leveraging *conditional types*. Let’s write a type alias that takes in a type `T` and converts it into a `SliceOfCake` if `T` is `Cake` or `never` if `T` is not `Cake`:

```tsx
type Slice<T> = T extends Cake ? SliceOfCake : never;
```

As you can see, conditional types borrow their syntax from ternary expressions in JavaScipt. Now if we pass in `Cake` (or any subtype of `Cake`) to `Slice` we get back `SliceOfCake`:

```tsx
type Result = Slice<Cake> // Returns "SliceOfCake"
```

We can nest conditional expressions to make `Slice` aware of both `Bread` and `Cake`:

```tsx
type Slice<V> = V extends Cake
  ? SliceOfCake
  : V extends Bread
    ? SliceOfBread
    : never;
```

Now if we pass in `Bread` or `Cake` to `Slice` get back `SliceOfBread` or `SliceOfCake`, respectively:

```tsx
type Result1 = Slice<Bread> // "SliceOfBread"
type Result2 = Slice<Cake> // "SliceOfCake"
type Result3 = Slice<Cereal> // "never"
```

We can use conditional types in combination with string literal types to start producing functions with powerful type inference.

Let’s take our `KindsOfBread` type from earlier and compliment it with a `KindsOfCake` type to rewrite `Slice`, only this time `Slice` will take in a string literal type and produce either `SliceOfBread` if we pass in a kind of bread or `SliceOfCake` if we pass in a kind of cake:

```tsx
type KindsOfBread =
  | 'Wheat'
  | 'White'
  | 'Rye'
  | 'Pumperknickel'
  | 'Sourdough'
  | 'Banana';

type KindsOfCake =
  | 'Vanilla'
  | 'Chocolate'
  | 'Strawberry'
  | 'Pound'
  | 'Coffee';

type Slice<T> = T extends KindsOfBread
  ? SliceOfBread
  : T extends KindsOfCake
    ? SliceOfCake
    : never;
```

Let’s see what we get back now:

```tsx
type Result1 = Slice<'Banana'> // "SliceOfBread"
type Result2 = Slice<'Vanilla'> // "SliceOfCake"
type Result3 = Slice<'Tuna'> // "never"
```

This works great, but there’s still an *aesthetic* problem with the code. No one writes out “Vanilla” or “Banana” and expects you to know they are talking about cakes and breads. Aesthetically, this code would be more pleasing if we wrote it out like this:

```tsx
type Result1 = Slice<'Banana Bread'>;
type Result2 = Slice<'Vanilla Cake'>;
type Result3 = Slice<'Tuna Fish'>;
```

How can we extract the first part of the string literal types (the *kind*) to figure out what we are returning? In TypeScript, expressions passed to conditional types can use inference to *infer* new types.

To take advantage of this, let’s write out a type for the categories of foods our application supports:

```tsx
type Foods = 'Bread' | 'Cake' | 'Fish';
```

Now let’s write a type that extracts the *kind* modifier from a type literal like `"Tuna Fish"`:

```tsx
type ExtractKind<V> = V extends `${infer Kind} ${Foods}`
  ? Kind
  : never;
```

What’s this doing? We are testing if the type parameter `V` is a string literal type in the format of `${Kind} ${Foods}`. For example, if we pass in `"Tuna Fish"` we will get back `"Tuna"` as the inferred type `Kind`. If we pass in just `"Tuna"` we will get back `never` since the string literal type `"Tuna"` is not in the format of `"Tuna Fish"`. Using this, we can now improve the aesthetics of `Slice`:

```tsx
type Slice<T, V = ExtractKind<T>> = V extends KindsOfBread
  ? SliceOfBread
  : V extends KindsOfCake
    ? SliceOfCake
    : never;

type Result1 = Slice<'Banana Bread'> // "SliceOfBread"
type Result2 = Slice<'Vanilla Cake'> // "SliceOfCake"
type Result3 = Slice<'Tuna Fish'> // "never"
```

NgRx’s `ActionName` needs to trim string literal types before doing any further conversion. It’s trimming strings by applying the exact same string inference trick that our `ExtractKind` utility is using by recursively inferring the string surrounded by whitespace:

```tsx
type Trim<T extends string> = T extends ` ${infer R}`
  ? Trim<R>
  : T extends `${infer R} `
    ? Trim<R>
    : T;
```

If you pass in `"  Banana Cake   "` to `Trim` you get back `"Banana Cake"`. Powerful TypeScript magic!

### 3. Lowercasing String Literal Types

With our bread sliced and our strings trimmed, we are ready to move on to the next bit of TypeScript behind `ActionName`: lowercasing string literal types!

How could we get from `"Banana Bread"` to `"banana bread"`? We could write out a very long and complex conditional type that maps each uppercase character into a lowercase character. Thankfully, however, TypeScript gives us a `Lowercase` utility type out-of-the-box. 🙂

```tsx
type Result = Lowercase<"Banana Bread"> // "banana bread"
```

Lowercasing? Easy! TypeScript ships with four utility types for manipulating string literal types:

* `Lowercase<"Banana Bread">` to produce `"banana bread"`
* `Uppercase<"Banana Bread">` to produce `"BANANA BREAD"`
* `Capitalize<"banana">` to produce `"Banana"`
* `Uncapitalize<"BANANA">` to produce `"bANANA"`

### 4. Titlecasing String Literal Types

TypeScript ships with utility types to lowercase, uppercase, capitalize, and uncapitalize string literal types. It does not include string literal types to do more advanced string manipulation.

For NgRx, we ultimately want to convert a string of words describing an event into a camelCased function name. To get there, we need to first convert the words into title case. In other words, go from `"banana bread"` to `"Banana Bread"`.

Before we build a titlecasing type utility, we need to explore *template literal types*. A template literal type is a supercharged string literal type that uses string interpolation syntax to create new string literal types. In our program, we have a `KindsOfBread` type that is a union of all of the kinds of breads our program is aware of. We could expand this into a type that includes the word `"Bread"` by using a template literal type:

```tsx
type Bread = `${KindsOfBread} Bread`;
```

This would be the same as writing:

```tsx
type Bread =
  | "Wheat Bread"
  | "White Bread"
  | "Rye Bread"
  | "Pumperknickel Bread"
  | "Sourdough Bread"
  | "Banana Bread";
```

Using template literal types, we can strengthen the clarity of our `Slice` type:

```tsx
type Bread = `${KindsOfBread} Bread`;
type Cake = `${KindsOfCake} Cake`;

type Slice<T extends Bread | Cake, V = ExtractKind<T>> = V extends KindsOfBread
  ? SliceOfBread
  ? V extends KindsOfCake
    ? SliceOfCake
    : never;
```

Our types continue to infer correctly:

```tsx
type Result1 = Slice<'Banana Bread'> // SliceOfBread
type Result2 = Slice<'Coffee Cake'> // SliceOfCake
```

And now if we try to pass in a food item that is not bread or cake we get a better error:

```
Type '"Tuna Fish"' does not satisfy the constraint '"Wheat Bread" | "White Bread" | "Rye Bread" | "Pumperknickel Bread" | "Sourdough Bread" | "Banana Bread" | "Vanilla Cake" | "Chocolate Cake" | "Strawberry Cake" | "Pound Cake" | "Coffee Cake"'.
```

Template literal types let us *expand* unions of string literal types into new unions of string literals. We can build a titlecasing type utility using TypeScript’s built-in string literal type utilities, conditional types, and template literal types:

```tsx
type Titlecase<T extends string> = T extends `${infer First} ${infer Rest}`
  ? `${Capitalize<First>} ${Titlecase<Rest>}`
  : Capitalize<T>;
```

Our `Titlecase` utility is doing the following:

1. Splitting up a string like `"banana nut bread"` into two types, `First` which is `"banana"` and `Rest` which is `"nut bread"`
2. It passes `First` to `Capitalize` and `Rest` to `Titlecase` for recursive processing
3. Once it gets to the very last word in the string literal type (in this case `"bread"`) it passes it to `Capitalize`

Now we can convert any string literal type into a titlecased string literal type:

```tsx
type Result = Titlecase<"banana nut bread"> // "Banana Nut Bread"
```

### 5. Removing Spaces Between Words

We can convert a string literal type that uses mixed casing with padded whitespace into a trimmed, titlecased string using the builtin `Lowercase` and our handwritten `Trim` and `Titlecase` type aliases:

```tsx
type R = Titlecase<Lowercase<Trim<"  banana NUT bread ">>> // "Banana Nut Bread"
```

We are still trying to get this to be in the form of `"bananaNutBread"` meaning we have to strip the spaces between words. Thankfully, we don’t need to learn any new tricks. We have everything we need with conditional types and template literal types:

```tsx
type RemoveSpaces<T extends string> = T extends `${infer First} ${infer Rest}`
  ? `${First}${RemoveSpaces<Rest>}`
  : T;
```

This is very similar to `Titlecase`, only this time we are not doing any additional string manipulation. All this type utility does is take a string literal type in the form of `"Banana Nut Bread"` and convert it into `"BananaNutBread"`.

### 6. Lowercasing the First Letter

We are *so close* now to having the ability to go from `"  banana NUT bread "` to `"bananaNutBread"`. All we are missing is a way to uncapitalize the first letter. And if you recall, TypeScript ships with a type utility to do just that! Now we can write out our full `ActionName` utility using the built-in `Lowercase` and `Uncapitalize` in combination with our `Trim`, `Titlecase`, and `RemoveSpaces` type utilities:

```tsx
type ActionName<T extends string> =
  Uncapitalize<RemoveSpace<Titlecase<Lowercase<Trim<T>>>>>
```

🥳🎉🥖

## Conclusion

NgRx’s `createActionGroup` relies on advanced TypeScript typing to convert the names of events into strongly-typed function names. It is able to cast from `"Event Name"` to `"eventName"` through a combination of string literal types, conditional types, and template literal types. I want to give a huge shout out to [Marko Stanimirović](https://dev.to/markostanimirovic) for turning this concept into a fully functioning and well tested NgRx feature. Check out the full source code if you want to see the rest of the type magic going on under-the-hood of `createActionGroup`.