Don't use TypeScript types like this. Use Map Pattern instead
Nikola Perišić

Nikola Perišić @perisicnikola37

About: Software Engineer | Technical Writer | 135k+ Reads

Location:
Podgorica, Montenegro
Joined:
Jun 27, 2023

Don't use TypeScript types like this. Use Map Pattern instead

Publish Date: Jan 29
216 33

Introduction

While working on a real-life project, I came across a particular TypeScript implementation that was functional but lacked flexibility. In this blog, I'll walk you through the problem I encountered, and how I improved the design by making a more dynamic approach using the Map Pattern.

Table of Contents

  1. The problem
  2. The issue with this approach
  3. Solution
  4. Clean code
  5. More secure solution
  6. Visual representation
  7. Conslusion

The problem

I came across this TypeScript type:

// FinalResponse.ts
import { Reaction } from './Reaction'

export type FinalResponse = {
  totalScore: number
  headingsPenalty: number
  sentencesPenalty: number
  charactersPenalty: number
  wordsPenalty: number
  headings: string[]
  sentences: string[]
  words: string[]
  links: { href: string; text: string }[]
  exceeded: {
    exceededSentences: string[]
    repeatedWords: { word: string; count: number }[]
  }
  reactions: {
    likes: Reaction
    unicorns: Reaction
    explodingHeads: Reaction
    raisedHands: Reaction
    fire: Reaction
  }
}
Enter fullscreen mode Exit fullscreen mode

Additionally, this Reaction type was defined:

// Reaction.ts
export type Reaction = {
  count: number
  percentage: number
}
Enter fullscreen mode Exit fullscreen mode

And this was being used in a function like so:

// calculator.ts
export const calculateScore = (
  headings: string[],
  sentences: string[],
  words: string[],
  totalPostCharactersCount: number,
  links: { href: string; text: string }[],
  reactions: {
    likes: Reaction
    unicorns: Reaction
    explodingHeads: Reaction
    raisedHands: Reaction
    fire: Reaction
  },
): FinalResponse => {
  // Score calculation logic...
}
Enter fullscreen mode Exit fullscreen mode

The Issue with This Approach

Now, imagine the scenario where the developer needs to add a new reaction (e.g., hearts, claps, etc.).
Given the current setup, they would have to:

  • Modify the FinalResponse.ts file to add the new reaction type.
  • Update the Reaction.ts type if necessary.
  • Modify the calculateScore function to include the new reaction.
  • Possibly update other parts of the application that rely on this structure.

So instead of just adding the new reaction in one place, they end up making changes in three or more files, which increases the potential for errors and redundancy. This approach is tightly coupled.

Solution

I came up with a cleaner solution by introducing a more flexible and reusable structure.

// FinalResponse.ts
import { Reaction } from './Reaction'

export type ReactionMap = Record<string, Reaction>

export type FinalResponse = {
  totalScore: number
  headingsPenalty: number
  sentencesPenalty: number
  charactersPenalty: number
  wordsPenalty: number
  headings: string[]
  sentences: string[]
  words: string[]
  links: { href: string; text: string }[]
  exceeded: {
    exceededSentences: string[]
    repeatedWords: { word: string; count: number }[]
  }
  reactions: ReactionMap
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • ReactionMap: This type uses Record<string, Reaction>, which means any string can be a key, and the value will always be of type Reaction.
  • FinalResponse: Now, the reactions field in FinalResponse is of type ReactionMap, allowing you to add any reaction dynamically without having to modify multiple files.

Clean code

In the calculator.ts file, the function now looks like this:

// calculator.ts
export const calculateScore = (
  headings: string[],
  sentences: string[],
  words: string[],
  totalPostCharactersCount: number,
  links: { href: string; text: string }[],
  reactions: ReactionMap,
): FinalResponse => {
  // Score calculation logic...
}
Enter fullscreen mode Exit fullscreen mode

But Wait! We Need Some Control

Although the new solution provides flexibility, it also introduces the risk of adding unchecked reactions, meaning anyone could potentially add any string as a reaction. We definitely don't want that.

To fix this, we can enforce stricter control over the allowed reactions.

More secure solution

Here’s the updated version where we restrict the reactions to a predefined set of allowed values:

// FinalResponse.ts
import { Reaction } from './Reaction'

type AllowedReactions =
  | 'likes'
  | 'unicorns'
  | 'explodingHeads'
  | 'raisedHands'
  | 'fire'

export type ReactionMap = {
  [key in AllowedReactions]: Reaction
}

export type FinalResponse = {
  totalScore: number
  headingsPenalty: number
  sentencesPenalty: number
  charactersPenalty: number
  wordsPenalty: number
  headings: string[]
  sentences: string[]
  words: string[]
  links: { href: string; text: string }[]
  exceeded: {
    exceededSentences: string[]
    repeatedWords: { word: string; count: number }[]
  }
  reactions: ReactionMap
}
Enter fullscreen mode Exit fullscreen mode

Visual representation

TypeScript Types

TypeScript Types

Conclusion

This approach strikes a balance between flexibility and control:

  • Flexibility: You can easily add new reactions by modifying just the AllowedReactions type.
  • Control: The use of a union type ensures that only the allowed reactions can be used, preventing the risk of invalid or unwanted reactions being added.

This code follows the Open/Closed Principle (OCP) by enabling the addition of new functionality through extensions, without the need to modify the existing code.

With this pattern, we can easily extend the list of reactions without modifying too many files, while still maintaining strict control over what can be added.

Code?

You can visit the repository here.


Hope you found this solution helpful! Thanks for reading. 😊

Follow me on GitHub


Comments 33 total

  • Himanshu Sorathiya
    Himanshu Sorathiya Jan 29, 2025

    It's absolutely correct, and it's very easy to see which properties are in this, manageable
    Gonna use this

  • JoelBonetR 🥇
    JoelBonetR 🥇Jan 29, 2025

    Good post, thanks for sharing! 😃👍🏻

  • Yannick Napsuciale
    Yannick NapsucialeJan 31, 2025

    Nice

  • Alex Lohr
    Alex LohrJan 31, 2025

    But wait, at some point, we need to give the user a list of available reactions, right? So we already have an array containing them. We should use that to construct our map:

    export const userReactions = [...] as const; 
    
    export type AllowedReaction = typeof userReactions[number];
    
    Enter fullscreen mode Exit fullscreen mode
    • Nikola Perišić
      Nikola PerišićJan 31, 2025

      Hi, Alex. Yes, that is possible, if I fetch them from the database for example. But what when not? For case when I get the reactions list from the user?

      For example: If I used Caido tool (security auditing toolkit) and append some non existing reaction, in your case, it would be added to the ReactionMap without check. That would require manual checking or AllowedReactions.

      In my case the AllowedReactions are the predefined reactions available on Dev.to, and they are specified in the FinalResponse type. The list of reactions is passed to the user through the reactions property.

      This is, I would say, a more advanced project with a lot of data parsing and calculation processes.
      In this project, the reactions received on certain dev.to blog post are mapped and then the structure like this is created:

      {
          "article_reaction_counts": [
              {
                  "category": "like",
                  "count": 596,
                  "percentage": 73
              },
              {
                  "category": "unicorn",
                  "count": 11,
                  "percentage": 1
              },
              {
                  "category": "exploding_head",
                  "count": 17,
                  "percentage": 2
              },
              {
                  "category": "raised_hands",
                  "count": 18,
                  "percentage": 2
              },
              {
                  "category": "fire",
                  "count": 19,
                  "percentage": 2
              },
              {
                  "category": "readinglist",
                  "count": 160,
                  "percentage": 19
              }
          ]
      }
      
      Enter fullscreen mode Exit fullscreen mode

      If you are interested you can check the code in the repository.

      • This is how I pass it to the user -> code

      You can also try it out here: dev-to-rater.xyz

      • Alex Lohr
        Alex LohrFeb 1, 2025

        If you add another reaction to the array of existing reactions, it is obviously an existing reaction itself. Saying the types are more true than your data is a fallacy.

        A single source of truth reduces the chances of errors and saves time if you have to change something.

        • Nathaniel Gott
          Nathaniel GottFeb 18, 2025

          You’re assuming we are getting data from a database, no? What if we are hardcoding the reactions into the app and want to use it in multiple locations?

          • Alex Lohr
            Alex LohrFeb 18, 2025

            I'm not assuming anyting. At some point, you want to show the reactions in your component, so you will have that array, regardless of how it came to be.

  • Manuchehr
    ManuchehrFeb 1, 2025

    There are bunch of bad codes there too. For example you can just use Record instead of

    export type ReactionMap = {
    
    }
    
    Enter fullscreen mode Exit fullscreen mode
    export type ReactionMap = Record<AllowedReactions, Reaction>
    
    Enter fullscreen mode Exit fullscreen mode

    You should also separate links according to your article

    • Manuchehr
      ManuchehrFeb 1, 2025

      Plus you can use interface for objects instead of types

      • Nikola Perišić
        Nikola PerišićFeb 1, 2025

        I used type because it is immutable. Unlike interface, type cannot be extended later in the code and I wanted to ensure that ReactionMap structure remains same and does not change somewhere else in the code. Thanks for the comment

        • Manuchehr
          ManuchehrFeb 1, 2025

          It's not actually accurate I'm afraid do a little research

          • Adam Donly
            Adam DonlyFeb 24, 2025

            What a horrible comment, what is wrong with you...if you don't have anything nice to say or constructive feedback maybe don't say anything

      • Slar
        SlarFeb 10, 2025

        Hey. Care to explain why we should use interface instead of type?

        • Manuchehr
          ManuchehrFeb 15, 2025

          I said you CAN not you SHOULD

          • Marcus Klausen
            Marcus KlausenMar 16, 2025

            So you just mentioned it for no reason? There's pretty wide consensus that types should be used until you actually need an interface. I don't see any inheritance, so I guess you're either not as smart as you think or you simply pulled it out of your ass for no apparent reason.

      • GNU Jesus
        GNU JesusMar 27, 2025

        But why types? I'm inclined to use types instead of interfaces since interfaces are normally used for functionality (e.g, That class has to implement these methods).

    • Nikola Perišić
      Nikola PerišićFeb 1, 2025

      This is better. Thanks!

      export type ReactionMap = Record<AllowedReactions, Reaction>
      
      Enter fullscreen mode Exit fullscreen mode
      • Manuchehr
        ManuchehrFeb 1, 2025

        good luck mate. Dont stop writing

  • Mahmoud Alaskalany
    Mahmoud AlaskalanyFeb 7, 2025

    Good post very helpful and thanks to all the people in the comments that are suggesting and improving it

  • АнонимFeb 7, 2025

    [hidden by post author]

  • АнонимFeb 8, 2025

    [hidden by post author]

  • АнонимFeb 9, 2025

    [hidden by post author]

  • Nahidul Islam
    Nahidul IslamFeb 10, 2025

    Great one!

  • Slar
    SlarFeb 10, 2025

    Cool! I was doing this without even acknowledging it. It's nice to give it a name (Map Pattern) so it feels solid. Definetely something everyone should know and apply.

    I usually kind of skip the "control" part and just use Record<string, Whatever> and forget about it. I don't even use the extra type ReactionMap, I just straight go

    reactions: Record<string, Reaction>
    
    Enter fullscreen mode Exit fullscreen mode

    Although yeah, for bigger projects the control should be included without a doubt.

    Nice post my man.

    • Nikola Perišić
      Nikola PerišićFeb 11, 2025

      Thanks for the comment and feedback man. Yes, it sounds solid. This is a problem that many people get wrong because they don't think about extensibility.

  • Mehdi Messaadi
    Mehdi MessaadiFeb 12, 2025

    Thanks, this a great tip!

Add comment