DEV Community

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

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

Nikola Perišić on January 29, 2025

Introduction While working on a real-life project, I came across a particular TypeScript implementation that was functional but lacked f...
Collapse
 
manuchehr profile image
Manuchehr

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

Collapse
 
perisicnikola37 profile image
Nikola Perišić • Edited

This is better. Thanks!

export type ReactionMap = Record<AllowedReactions, Reaction>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
manuchehr profile image
Manuchehr • Edited

good luck mate. Dont stop writing

Collapse
 
manuchehr profile image
Manuchehr

Plus you can use interface for objects instead of types

Collapse
 
perisicnikola37 profile image
Nikola Perišić • Edited

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

Thread Thread
 
manuchehr profile image
Manuchehr

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

Thread Thread
 
adamdonly23 profile image
Adam Donly

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

Collapse
 
gnujesus profile image
GNU Jesus

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

Collapse
 
slar_ee98aa75925 profile image
Slar

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

Thread Thread
 
manuchehr profile image
Manuchehr

I said you CAN not you SHOULD

Thread Thread
 
marcus_klausen_24f512746f profile image
Marcus Klausen

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.

Collapse
 
lexlohr profile image
Alex Lohr

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
Collapse
 
perisicnikola37 profile image
Nikola Perišić • Edited

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

Collapse
 
lexlohr profile image
Alex Lohr

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.

Thread Thread
 
nathaniel_gott_bb45c5540d profile image
Nathaniel Gott

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?

Thread Thread
 
lexlohr profile image
Alex Lohr

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.

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Good post, thanks for sharing! 😃👍🏻

Collapse
 
perisicnikola37 profile image
Nikola Perišić • Edited

You're welcome! If you want to view it in a real action, feel free to do it in the repository.
Specifically, the files are:

Thanks for reading :)

Collapse
 
himanshu_code profile image
Himanshu Sorathiya

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

Collapse
 
perisicnikola37 profile image
Nikola Perišić

Thanks for feedback. Glad it was useful

Collapse
 
slar_ee98aa75925 profile image
Slar

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.

Collapse
 
perisicnikola37 profile image
Nikola Perišić

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.

Collapse
 
toothlesstarantula profile image
Yannick Napsuciale

Nice

Collapse
 
perisicnikola37 profile image
Nikola Perišić

Glad you liked it

Collapse
 
nahidulislam profile image
Nahidul Islam

Great one!

Collapse
 
perisicnikola37 profile image
Nikola Perišić

Thank you!

Collapse
 
mahmoudalaskalany profile image
Mahmoud Alaskalany

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

Collapse
 
perisicnikola37 profile image
Nikola Perišić

Yes, that is key. To help each other

Collapse
 
mehdi_messaadi_5ef3ac67bb profile image
Mehdi Messaadi

Thanks, this a great tip!

Collapse
 
perisicnikola37 profile image
Nikola Perišić

You're welcome!

Collapse
 
realsergiy profile image
Info Comment hidden by post author - thread only accessible via permalink
realSergiy

You left some room for improvements:

  • precise namings are life
  • single object argument parameter are better: don't enforce order, promote naming uniformity etc.
  • explicit function return types are evil

How about

type ReactionScore = {
  count: number
  percentage: number
}

type Reaction = 'likes' | 'unicorns' | 'explodingHeads' | 'raisedHands' | 'fire'
type Reactions = Record<Reaction, ReactionScore>

type Score = {
  headings: string[]
  sentences: string[]  
  words: string[]  
  links: { href: string; text: string }[]
  reactions: Reaction
}

type CalculateScoreOptions = {
  totalPostCharactersCount: number  
} & Score

const calculateScore = ({
  headings,
  sentences,
  words,
  totalPostCharactersCount,
  links,
  reactions
  } : CalculateScoreOptions
) => {
  // logic returning
  // type FullScore = {
  //   ...rest
  // } & Score  
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
oleasteo profile image
Info Comment hidden by post author - thread only accessible via permalink
Ole Asteo

Further improvement: Use enum instead of type for the AllowedReactions — which I'd rather call something along the lines of ReactionKind instead. This will improve the developer experience, e.g. for searching references to a specific kind. Btw, always use string enums.

Also, side note: I would add another reason why Record<string, X> is generally something you'd want to avoid. That type is tricky as it behaves as Partial<Record<string, X>> for definition (not every string need be defined), but Record<string, X> for access. This, in turn, results in unsafe access operations that may crash at runtime: Not every valid string is a key that yields a value X.
In general, most Record types that ain't Record<enum, X> should be Partial<Record<...>> instead.
You might even want to define it as a helper type

type PRecord<Key extends string | number | symbol, Value> = Partial<Record<Key, Value>>
Enter fullscreen mode Exit fullscreen mode

for convenience.

Collapse
 
dimo_zaprianov_ece4ac679e profile image
Info Comment hidden by post author - thread only accessible via permalink
Dimo Zaprianov

Why not just define a common type that include all reactions and use it in both places

Some comments have been hidden by the post's author - find out more