{"id":14705,"date":"2023-05-17T00:00:00","date_gmt":"2023-05-15T19:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/cse\/?p=14705"},"modified":"2024-07-18T11:50:48","modified_gmt":"2024-07-18T18:50:48","slug":"getting-to-know-typescript-generics","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/getting-to-know-typescript-generics\/","title":{"rendered":"Getting to Know TypeScript Generics"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>DRY. Do Not Repeat Yourself. DRY is an acronym that became synonymous with engineering fundamentals. Using DRY, we ensure that we don&#8217;t find ourselves with the same lines of code repeated over and over in a repository; indeed when the inevitable need arises to add a single tweak, we come across the predicament of having to go back to each-and-every-single-repeated piece of code to update each individually.<\/p>\n<p>Repeating code was a challenge we quickly came to face during an engagement with a customer where we needed to implement card component selection functionality using <a href=\"https:\/\/react.dev\/reference\/react\">React Hooks<\/a>. The reason we had to repeat ourselves, was that in multiple instances we needed to implement the card selection state to handle multiple data types (<code>string<\/code>, <code>number<\/code>, etc.). While in the midst of the <code>ctrl+c, ctrl+p<\/code> routine, we decided to refactor our components to be more maintainable and properly &#8220;reusable&#8221; to embrace React&#8217;s full capability of custom hooks, while still allowing us to keep our type safety in the realm of <code>TypeScript.<\/code><\/p>\n<p>Thus, we had the opportunity to leverage <code>TypeScript Generics<\/code> as a way to keep DRY, increase maintainability, and keep in the recommended pattern of React Reusable components.<\/p>\n<p>Below, this post will take you through examples of where <code>TypeScript Generics<\/code> can help DRY up your code, while demonstrating both a generic TypeScript function sample and a React Custom Hook example that follows a closer approach that was used during our customer engagement.<\/p>\n<h2>TypeScript Generic Overview<\/h2>\n<p>A TypeScript generic is a way of creating reusable components that can work with different types of data rather than one. It allows you to specify a placeholder type that represents the actual type that will be used when the code is executed. For example, you can define an array as <code>Array&lt;T&gt;<\/code> where T is any type you want.<\/p>\n<h2>TLDR Sample<\/h2>\n<p>If you prefer to play around with an example, refer to this <a href=\"https:\/\/codesandbox.io\/p\/sandbox\/hbl9nl?file=%2FREADME.md\">Code Sandbox React Hook Example<\/a><\/p>\n<h2>Problem Example<\/h2>\n<p>Let&#8217;s say that we have two functions that virtually do the same thing, however the parameter may have multiple types. Let&#8217;s say that we have a business, and we sell <strong>bikes<\/strong> and <strong>tennis racquets<\/strong>. Now, both items are different, albeit they both have a <code>price<\/code> tag attached to them.<\/p>\n<p>So we have a bike <code>type<\/code>:<\/p>\n<pre><code class=\"language-typescript\">type Bike = {\r\n  price: number;\r\n  name: string;\r\n  productType: number; \/\/enum 1 - Electric, 2 - Mechanical\r\n  tires: string;\r\n  weight: number;\r\n  length: number;\r\n  height: number;\r\n}<\/code><\/pre>\n<p>and a tennis racquet <code>type<\/code>:<\/p>\n<pre><code class=\"language-typescript\">type TennisRacquet = {\r\n  name: string;\r\n  price: number;\r\n  brand: number;  \/\/ enum 1 - Babolat,  2 - Head, 3- Wilson\r\n  weight: number;\r\n  gripSize: number;\r\n  size: number;\r\n  racquetStrings: []string;\r\n}<\/code><\/pre>\n<p>Both <code>Bike<\/code> and <code>TennisRacquet<\/code> are pretty different in types, however they have a <code>price<\/code> in common.\nTo find the average cost of our <code>TennisRacquets<\/code> and <code>Bikes<\/code>, we have 2 functions:<\/p>\n<pre><code class=\"language-ts\">\/\/ Average Bike Cost\r\nconst averageBikeCost: number = (bikes: Bike[]) =&gt; {\r\n    const sum: number = bikes.map(bike =&gt; bike.price).reduce((val, sum) =&gt; val + sum, 0);\r\n    const average = sum \/ bikes.length;\r\n    return average\r\n}\r\n\r\n\/\/ Average Tennis Racquet Cost\r\nconst averageTennisRacquetCost: number = (tennisRacquets: TennisRacquet[]) =&gt; {\r\n    const sum: number = tennisRacquets.map(tennisRacquet =&gt; tennisRacquet.price).reduce((val, sum) =&gt; val + sum, 0);\r\n    const average = sum \/ tennisRacquets.length;\r\n    return average\r\n}<\/code><\/pre>\n<p>Both these functions look fairly identical, except for the type. We could combine these two functions, without much difficulty since both just use the <code>price<\/code>.<\/p>\n<pre><code class=\"language-ts\">const averageProductCost: number = (products: TennisRacquet[] | Bike[]) =&gt; {\r\n    const sum: number = products.map(product =&gt; product.price).reduce((val, sum) =&gt; val + sum, 0);\r\n    const average = sum \/ products.length;\r\n    return average\r\n}<\/code><\/pre>\n<p>However, to introduce a bit more complex functionality to get a mapping of the average of each &#8220;type&#8221; of racquet and bike, E.g. The average cost of all the electric bikes, and the average cost of all Babolat racquets.<\/p>\n<p>If we want to wrap this in one function, countSumMap, we will have to type narrow as shown below.<\/p>\n<p>Now Imagine if we want to extend this by another type if we add to our inventory of products (E.g., type <code>Shoes<\/code>). We would be increasing this example quite a bit, and it will feel very repetitive<\/p>\n<pre><code class=\"language-ts\">const countSumMap: Map&lt;string, { count: number; sum: number }&gt; = (prods: Tennis[] | Bike[]) =&gt; {\r\n    const newMap = new Map&lt;string, { count: number; sum: number }&gt;();\r\n    if (\"tires\" in prods[0]) {\r\n        prods.map(prod =&gt; {\r\n          if (newMap.has(prod.productType)){\r\n              const { count, sum } = newMap.get(prod.productType)\r\n              newMap.set(prod.productType, { count: count + 1, sum: prod.price + sum });\r\n          } else {\r\n              newMap.set(prod.productType, { count: 1, sum: prod.price });\r\n          }\r\n        })\r\n    } else if (\"handleSize\" in prods[0]) {\r\n        prods.map(prod =&gt; {\r\n            if (newMap.has(prod.racquetBrand)) {\r\n                const { count, sum } = newMap.get(prod.racquetBrand)\r\n                newMap.set(prod.racquetBrand, { count: count + 1, sum: prod.price + sum });\r\n            } else {\r\n                newMap.set(prod.racquetBrand, { count: 1, sum: prod.price });\r\n            }\r\n        })\r\n    } else if (\"shoeSize\" in prods[0]) {\r\n        \/\/ ... conditional logic for type Shoe \r\n    }\r\n    return newMap\r\n}\r\n\r\nconst tennisRacquetsCountPriceMap = countSumMap(tennisRacquets);\r\n\/\/ Map(1) { 'Babolat' =&gt; { count: 1, sum: 150 } }\r\n\/\/ ... extra logic to find average \/ mean price\r\nconst bikeCountPriceMap = countSumMap(bikes)\r\n\/\/ Map(1) { 'Electric' =&gt; { count: 2, sum: 1500 } }\r\n\/\/ ... extra logic to find average \/ mean price<\/code><\/pre>\n<p>This is where Typescript Generics come in handy!<\/p>\n<h2>Using Generics<\/h2>\n<p>We can use a type Generic <code>&lt;T&gt;<\/code> to help us reduce this repetitive code.<\/p>\n<p>Notice the code shown below, this is much more concise, and also reusable.\n<code>countSumMap<\/code> can be used for any object with a <code>price<\/code>. <code>keyName<\/code> is given to allow us to define how we organize our summation and count.<\/p>\n<pre><code class=\"language-ts\">const countSumMap: Map&lt;string, { count: number; sum: number }&gt; = &lt;T&gt;(prods: T[], keyName: string) =&gt; {\r\n    if (!keyName) return new Map();\r\n\r\n    const newMap = new Map&lt;string, { count: number; sum: number }&gt;();\r\n        prods.map(prod =&gt; {\r\n            if (newMap.has(prod[keyName])){\r\n                const { count, sum } = newMap.get(prod[keyName])\r\n                newMap.set(prod[keyName], { count: count + 1, sum: prod.price + sum });\r\n            } else {\r\n                newMap.set(prod[keyName], { count: 1, sum: prod.price });\r\n            }\r\n        })\r\n    return newMap\r\n}\r\n\/\/ console.log(countSumMap(bikes, \"productType\"))\r\n\/\/ Map(2) {\r\n\/\/   'Electric' =&gt; { count: 2, sum: 1500 },\r\n\/\/   'Mechanical' =&gt; { count: 1, sum: 3000 }\r\n\/\/ }<\/code><\/pre>\n<p><code>countSumMap<\/code> will infer <code>&lt;T&gt;<\/code>, as whatever type we pass as our parameter to <code>prods<\/code>. If our <code>countSumMap<\/code> takes an argument of <code>Bike[]<\/code>, then it will be able to handle the logic while inferring <code>Bike<\/code> as the type used in the function body. The same goes with <code>TennisRacquet<\/code>.<\/p>\n<h2>Another Example &#8211; React Custom Hook<\/h2>\n<blockquote><p>NOTE: For a Full Working Example, See this <a href=\"https:\/\/codesandbox.io\/p\/sandbox\/hbl9nl?file=%2FREADME.md\">Code Sandbox Demo<\/a><\/p><\/blockquote>\n<p>For this example, assume that we are attempting to set up an item selection that allows us to define a <code>multiSelect<\/code> mode, so we can either add a single item as our <code>activeItem<\/code>, or we can select <code>multiple<\/code>.<\/p>\n<ul>\n<li>Think of holding <code>shift<\/code> to select multiple.<\/li>\n<\/ul>\n<p>The first step will be to create a <a href=\"https:\/\/beta.reactjs.org\/learn\/reusing-logic-with-custom-hooks\">custom hook<\/a> that will allow us to create a React hook that can receive any <code>type<\/code> in a <code>Set<\/code>, and allow items to be stored and modified in that <code>Set<\/code>. Our custom hook will also receive <code>multiSelect<\/code> boolean to define if we want to do a single active item, or multi select.<\/p>\n<pre><code class=\"language-TS\">import { useState } from \"react\";\r\n\r\ninterface ItemState&lt;T&gt; {\r\n    selectedItems: Set&lt;T&gt;;\r\n}\r\ninterface ItemActions&lt;T&gt; {\r\n    handleItemSelect: (item: T, multiSelectMode: boolean) =&gt; void;\r\n}\r\nexport const useSelect = &lt;T&gt;(items: Set&lt;T&gt;): [ItemState&lt;T&gt;, ItemActions&lt;T&gt;] =&gt; {\r\n    const [selectedItems, setSelectedItems] = useState&lt;typeof items&gt;(items);\r\n\r\n    const handleItemSelect = (\r\n        item: T,\r\n        multiSelectMode: boolean = false\r\n    ): void =&gt; {\r\n        const copiedSet = new Set&lt;T&gt;(selectedItems);\r\n        if (copiedSet.has(item)) {\r\n            copiedSet.delete(item);\r\n        } else {\r\n            if (!multiSelectMode) {\r\n                const newSet = new Set&lt;T&gt;();\r\n                newSet.add(item);\r\n                setSelectedItems(newSet);\r\n                return;\r\n            }\r\n            copiedSet.add(item);\r\n        }\r\n        setSelectedItems(copiedSet);\r\n    };\r\n\r\n    return [\r\n        {\r\n            selectedItems,\r\n        },\r\n        {\r\n            handleItemSelect,\r\n        },\r\n    ];\r\n};<\/code><\/pre>\n<p>To continue the concept of <code>Bikes<\/code> and <code>TennisRacquets<\/code>, if we have two distinct lists of items for our Bikes and Racquets, we could use the above code sample to handle selection for both <code>types<\/code>.<\/p>\n<h3>Tennis Racquet Example<\/h3>\n<pre><code class=\"language-TSX\">import { useSelect } from \"..\/hooks\";\r\n\r\nfunction TennisRacquetList({ racquets }: { racquets: ITennisRacquet[] }) {\r\n    const [{ selectedItems }, { handleItemSelect }] = useSelect(\r\n        new Set&lt;string&gt;()\r\n    );\r\n    return (\r\n        &lt;div className=\"card-wrapper\"&gt;\r\n            {racquets.map((racquet) =&gt; (\r\n                &lt;TennisRacquet\r\n                  key={racquet.GUID}\r\n                  isActive={selectedItems.has(racquet.GUID)}\r\n                  tennisRacquet={racquet}\r\n                  selectOne={handleItemSelect}\r\n                  multiSelect={handleItemSelect}\r\n                \/&gt;\r\n            ))}\r\n        &lt;\/div&gt;\r\n    );\r\n}\r\n\r\nexport default TennisRacquetList;<\/code><\/pre>\n<h3>Bike Example<\/h3>\n<pre><code class=\"language-TSX\">import { useSelect } from \"..\/hooks\";\r\n\r\nfunction Bikes({ bikes }: { bikes: IBike[] }) {\r\n    const [{ selectedItems }, { handleItemSelect }] = useSelect(\r\n        new Set&lt;number&gt;()\r\n    );\r\n    return (\r\n        &lt;div className=\"card-wrapper\"&gt;\r\n            {bikes.map((bike) =&gt; (\r\n                &lt;Bike\r\n                  key={bike.id}\r\n                  isActive={selectedItems.has(bike.id)}\r\n                  bike={bike}\r\n                  selectOne={handleItemSelect}\r\n                  multiSelect={handleItemSelect}\r\n                \/&gt;\r\n            ))}\r\n        &lt;\/div&gt;\r\n    );\r\n}\r\n\r\nexport default Bikes;<\/code><\/pre>\n<h2>Summary<\/h2>\n<p>Generic Typescript types allow reusable functionality for logic that may be very similar but only differ by type. Instead of needing to create <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/2\/narrowing.html#using-type-predicates\"><strong>type predicates<\/strong><\/a> or type narrowing in this use-case, we can instead use <code>TypeScript Generics<\/code> to achieve reusability, and maintainability in the instance that another type is requesting this same functionality.<\/p>\n<h2>References<\/h2>\n<ul>\n<li><a href=\"https:\/\/www.javatpoint.com\/typescript-generics\">TypeScript Generics Tutorial<\/a><\/li>\n<li><a href=\"https:\/\/www.educba.com\/typescript-generic\/\">TypeScript Generic Example<\/a><\/li>\n<li><a href=\"https:\/\/www.rainerhahnekamp.com\/en\/type-safe-typescript-with-type-narrowing\/\">Type Narrowing in TypeScript<\/a><\/li>\n<li><a href=\"https:\/\/beta.reactjs.org\/learn\/reusing-logic-with-custom-hooks\">Custom Hooks<\/a><\/li>\n<\/ul>\n<h2>Acknowledgements<\/h2>\n<p>A special thanks to the wonderful team behind this engagement and learning: <a href=\"https:\/\/www.linkedin.com\/in\/bryan77\/\">Bryan<\/a>, <a href=\"https:\/\/www.linkedin.com\/in\/ivan-sholokh\/\">Ivan<\/a>, <a href=\"https:\/\/www.linkedin.com\/in\/ranadeshreyas\/\">Shreyas<\/a>, <a href=\"https:\/\/www.linkedin.com\/in\/maggiemarxen\/\">Maggie<\/a>, <a href=\"https:\/\/www.linkedin.com\/in\/bstateham\/\">Bret<\/a>, <a href=\"https:\/\/www.linkedin.com\/in\/mscherotter\/\">Michael<\/a>, and <a href=\"https:\/\/www.linkedin.com\/in\/maartenvandebospoort\/\">Maarten<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A guide to using TypeScript Generics as a way to create reusable logic that will work for a multitude of types.<\/p>\n","protected":false},"author":117315,"featured_media":14713,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,17],"tags":[220,3301,3302],"class_list":["post-14705","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cse","category-frameworks","tag-javascript","tag-react","tag-typescript"],"acf":[],"blog_post_summary":"<p>A guide to using TypeScript Generics as a way to create reusable logic that will work for a multitude of types.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/14705","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/117315"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=14705"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/14705\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/14713"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=14705"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=14705"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=14705"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}