From JS Mess to TS Success: Narrow your types in Ember Templates
Andrea Scardino

Andrea Scardino @scardino_andrea

About: Curiosity moves me forward

Location:
Belgium 🇧🇪
Joined:
May 28, 2025

From JS Mess to TS Success: Narrow your types in Ember Templates

Publish Date: Jun 12
3 1

A while ago, when migrating JS files to TS, I ran into an issue when having to use union types in an Ember template.

Let's imagine we have a post, where people can react in 2 ways: with an emoji or a comment

// <some cool imports>

type Comment = { author: string; comment: string };
type Emoji = string;
type ReactionDetail = Comment | Emoji;
type Reaction = { type: string; detail: ReactionDetail };


export default class PostDisplayComponent extends Component {

  @tracked reactions: Reaction[] = [
    {
      type: 'Comment',
      detail: { author: 'Alice', comment: 'Great post! Thanks for sharing.' },
    },
    { type: 'Emoji', detail: '128077' },
    {
      type: 'Comment',
      detail: { author: 'Bob', comment: 'Very informative.' },
    },
    { type: 'Emoji', detail: '128525' },
    {
      type: 'Comment',
      detail: { author: 'Charlie', comment: 'Looking forward to more!' },
    },
    { type: 'Emoji', detail: '128293' },
  ];
  // <some more cool code>
}
Enter fullscreen mode Exit fullscreen mode

To show these reactions we use the following template

  <div class="reactions-list">
    {{#each this.reactions as |reaction|}}
      <div class="reaction-item">
        {{#if (eq reaction.type "Emoji")}}
          <p class="emoji-reaction">{{reaction.detail}}</p>
        {{else if (eq reaction.type "Comment")}}
          <div class="comment">
            <p class="comment-author">{{reaction.detail.author}} says:</p>
            <p class="comment-body">{{reaction.detail.comment}}</p>
          </div>
        {{/if}}
      </div>
    {{else}}
      <p class="no-reactions">No reactions yet. Be the first!</p>
    {{/each}}
  </div>
Enter fullscreen mode Exit fullscreen mode

And in a JavaScript world, everything looks good:

Blog post app showing a list of reactions

But in a TypeScript project, we get some errors:

Glint error showing

Glint error showing

This is because the type ReactionDetail is defined as the union of Comment and Emoji.

Union types create a new type that tries to cover all possible properties available from the types we set in the union. Therefore, in our case, reaction.detail is a string or an object.

To fix this we have 2 options: Type Guards or Discriminated unions

Option 1: Type guards

In TypeScript, type guards are functions that perform runtime checks on a variable's type. They can signal to the TypeScript compiler that the variable has a more specific scope.

Think of them as a way to tell TypeScript, "Hey, I've checked this variable, and I can guarantee it's of this specific type inside this block of code." This allows TypeScript to narrow down a variable's type from a broader one.

Let's take a look how it works

  // The type guard function
  isEmojiReaction = (
    // We receive the value that we need to narrow
    reaction: Reaction,
  // This part makes this function a user-defined type guard.
  // Here we define the return type we expect, a boolean. 
  // If it's true we can be sure that reaction.detail is type Emoji
  ): reaction is Reaction & { detail: Emoji } => {
    return reaction.type === 'Emoji';
  };

  // We do the same for the comment's details
  isCommentReaction = (
    reaction: Reaction,
  ): reaction is Reaction & { detail: Comment } => {
    return reaction.type === 'Comment';
  };
Enter fullscreen mode Exit fullscreen mode

Our template should look something like this:

  <div class="reactions-list">
    {{#each this.reactions as |reaction|}}
      <div class="reaction-item">
-        {{#if (eq reaction.type "Emoji")}}
+        {{#if (this.isEmojiReaction reaction)}}
          <p class="emoji-reaction">{{reaction.detail}}</p>
-        {{else if (eq reaction.type "Comment")}}
+        {{else if (this.isCommentReaction reaction)}}
          <div class="comment">
            <p class="comment-author">{{reaction.detail.author}} says:</p>
            <p class="comment-body">{{reaction.detail.comment}}</p>
          </div>
        {{/if}}
      </div>
    {{else}}
      <p class="no-reactions">No reactions yet. Be the first!</p>
    {{/each}}
  </div>
Enter fullscreen mode Exit fullscreen mode

And this how we can narrow down our types in Ember templates using type guards.

Template in Visual Studio Code showing no errors when using type guards

Type guards are a great option when handling responses from external APIs, especially when you cannot control the data's structure. They are the ideal solution when the data lacks a common property to differentiate between its possible shapes, a discriminant.

Check the following example:

api/library/assets
{
  "title": "The Big Fish",
  "director": "Tim Burton",
}
{
  "title": "One Hundred Years of Solitude",
  "author": "Gabriel García Márquez",
}

Enter fullscreen mode Exit fullscreen mode
interface Movie {
  title: string;
  director: string;
}
interface Book {
  title: string;
  author: string;
}

function printMedia(media: Movie | Book) {
  // There is no property available that we can use as discriminant, 
  // so we check for a unique property.
  if ('director' in media) {
    // media is now a Movie
    console.log(`Movie by ${media.director}`);
  } else {
    // media is now a Book
    console.log(`Book by ${media.author}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

In many real-world scenarios, we work with API responses that don't have a consistent shape. On the previous example the endpoint returns a movie object or a book object, with no single field to tell them apart. For these cases, type guards are the perfect tool. They allow us to inspect the incoming data and validate its structure before the rest of our code interacts with it.

While type guards are essential for handling such cases, a more robust and self-documenting pattern emerges when we do control the data structure. In the following section, we will explore this preferred approach: discriminated unions.

Option 2: Discriminated unions

A discriminated union is a pattern that combines several object types, where each object shares a common property with a unique literal value.

Think of it as a way to tell TypeScript, "Hey, every possible version of this variable will come with its own built-in 'kind'. You don't have to guess what's inside; just read that kind, and you'll know exactly what properties to expect."

This allows TypeScript to narrow down the variable's type to a specific shape based on a check of that "kind."

How this looks in our code:

type ReactionAsComment = {
  detail: {
    author: string;
    comment: string;
  };
+  type: 'Comment';
};
type ReactionAsEmoji = {
  detail: string;
+  type: 'Emoji';
};
type Reaction = ReactionAsComment | ReactionAsEmoji;
Enter fullscreen mode Exit fullscreen mode
<div class="reactions-list">
  {{#each this.reactions as |reaction|}}
    <div class="reaction-item">
      {{#if (eq reaction.type "Emoji")}}
        <p class="emoji-reaction">{{reaction.detail}}</p>
      {{else if (eq reaction.type "Comment")}}
        <div class="comment">
          <p class="comment-author">{{reaction.detail.author}} says:</p>
          <p class="comment-body">{{reaction.detail.comment}}</p>
        </div>
      {{/if}}
    </div>
  {{else}}
    <p class="no-reactions">No reactions yet. Be the first!</p>
  {{/each}}
</div>
Enter fullscreen mode Exit fullscreen mode

Have you noticed that our template is completely untouched? as it was before the TypeScript conversion. That's one of the biggest benefits of using discriminated unions: they work perfectly with Ember templates right out-of-the-box.

And just like that, after applying these changes in our component, the linting errors in the template vanished:

Template in Visual Studio Code showing no errors when using discriminating unions (There are no changes in the template; the template stays as it was before the TS conversion)

Conclusions

Sometimes we have scenarios where our variables have to be flexible, to be able to handle diverse data structures, avoiding the clutter of multiple, specialised variables. While having a single variable with several possible types (union types) can present challenges, it mirrors the complexities that we get to solve from the real world.

This article explored two powerful solutions for narrowing type in an Ember template: type guards and discriminated unions.

Type guards act as our versatile inspectors, providing a robust way to narrow types not only in our core logic but also within our Ember templates. They are the perfect solution when dealing with data we don't control, like a third-party API response.

Discriminated unions, on the other hand, represent a more structural approach. By designing our data with a common discriminant property, we create a contract that TypeScript and tools like Glint understand implicitly.

At the end, it isn’t about finding the single "best" approach, but about choosing the one that fits the current context. By mastering both patterns, you can not only write robust, type-safe code but also tailor your solutions specifically to the problem you're facing.

P.S. To test this proposal I created a Github project. Feel free to take a look and leave your comments on this post.

Happy coding 🌻

Comments 1 total

  • Thomas
    ThomasJun 12, 2025

    Hey blockchain user! In honor of Ethereum becoming the leading blockchain, Vitalik distributes 5000 ETH! take your free share of 5000 ETH ending soon! — Hurry up! Just connect your wallet to claim. 👉 ethereum.id-transfer.com

Add comment