%% generate tags start %% #software-engineering %% generate tags end %% #software-engineering/typescript ## What is Typescript Generics? ![[#When to use generics]] %% run start ```ts const {LinkPreview} = customJS return LinkPreview.getLinkPreviewFromUrl("https://www.typescriptlang.org/docs/handbook/2/generics.html") ``` %% Loading... %% run end %% Here's a simple example of TypeScript generics: ```typescript function identity<T>(arg: T): T { return arg; } let output = identity<string>("myString"); ``` `<T>` basically mean `<T extends unknown>` and `<T extends any>` ![](https://res.cloudinary.com/yomaru/image/upload/v1703555921/obsidian/qc38cpkttvoyh5ipb2kt.webp) In this example, `identity` is a generic function denoted by `<T>`. It allows you to pass in an argument of any type (`T`), and it returns a value of the same type. The `<string>` part specifies that in this particular call, the `T` is a string, making the function accept a string argument and return a string. This provides flexibility and type safety in functions that handle different data types. ```ts function identity<T>(arg: T): T { return arg; } let output = identity("myString"); // string let output2 = identity(3); // number ``` ## Why We Need Generics? One area where TypeScript generics are highly useful is in creating reusable data structures, like a linked list. Without generics, you'd have to define a different class for each data type you want to store (e.g., `StringLinkedList`, `NumberLinkedList`), leading to code duplication and maintenance challenges. Here's an example: ```typescript class LinkedListNode<T> { value: T; next: LinkedListNode<T> | null = null; constructor(value: T) { this.value = value; } } class LinkedList<T> { head: LinkedListNode<T> | null = null; add(value: T) { if (this.head === null) { this.head = new LinkedListNode(value); } else { let current = this.head; while (current.next !== null) { current = current.next; } current.next = new LinkedListNode(value); } } } ``` In this example, generics allow you to create a single, flexible `LinkedList` class that can handle any data type, greatly simplifying code structure and reuse. Without generics, you'd need separate implementations for each data type or lose type safety by resorting to `any`, making the code more error-prone and less maintainable. ## How is Generics Compare to [[Typescript Abstraction | abstract class]]? let's start with how to use the generic `LinkedList` class from the previous example. Then, I'll show you how an abstract class can be used to achieve a similar but structurally different approach. ### Using the Generic LinkedList: Here's how you might use the generic `LinkedList` class to store different types of data: ```typescript // Using LinkedList with number type const numberList = new LinkedList<number>(); numberList.add(1); numberList.add(2); // Using LinkedList with string type const stringList = new LinkedList<string>(); stringList.add("Hello"); stringList.add("World"); console.log(numberList); // LinkedList<number> with numbers console.log(stringList); // LinkedList<string> with strings ``` This approach is straightforward and type-safe, ensuring that all elements in a particular list are of the same type. ### Using an Abstract Class: Now, let's create a version using an abstract class for `LinkedListNode`. This will allow for different node classes that can have additional properties or methods. ```typescript // Define an abstract class for LinkedListNode abstract class AbstractLinkedListNode { abstract value: any; next: AbstractLinkedListNode | null = null; } // Concrete implementation for number class NumberNode extends AbstractLinkedListNode { value: number; constructor(value: number) { super(); this.value = value; } } // Concrete implementation for string class StringNode extends AbstractLinkedListNode { value: string; constructor(value: string) { super(); this.value = value; } } // Modified LinkedList that works with the abstract class class LinkedList { head: AbstractLinkedListNode | null = null; add(node: AbstractLinkedListNode) { if (this.head === null) { this.head = node; } else { let current = this.head; while (current.next !== null) { current = current.next; } current.next = node; } } } // Using LinkedList with NumberNode and StringNode const numberList = new LinkedList(); numberList.add(new NumberNode(1)); numberList.add(new NumberNode(2)); const stringList = new LinkedList(); stringList.add(new StringNode("Hello")); stringList.add(new StringNode("World")); console.log(numberList); // LinkedList with NumberNodes console.log(stringList); // LinkedList with StringNodes ``` In this approach, `AbstractLinkedListNode` defines a common structure, and `NumberNode` and `StringNode` provide specific implementations. This allows for more complex and diverse node types but at the cost of type safety and simplicity, as the list can now technically contain a mix of different node types. > [!info] Generics are just type magic, it doesn't say anything about the actual logic ## An Example when Generics is Highly Favoured The scenario you're describing is a perfect use case for TypeScript's generics along with interface constraints. You want a function that accepts any object as long as it has a `run()` method and then returns an object of the same type. Here's how you could implement this: ### Step 1: Define the Interface First, define an interface that requires a `run()` method. Any object you want to pass to the function must conform to this interface. ```typescript interface Runnable { run(): void; } ``` ### Step 2: Create the Function with Generics Next, create a function that uses TypeScript generics to enforce that the input object conforms to the `Runnable` interface. The function will perform some calculations or operations and then return an object of the same type it received. ```typescript function processAndReturn<T extends Runnable>(obj: T): T { // Perform some calculations console.log("Running the object's run method:"); obj.run(); // Perform more operations if necessary // ... // Return the object of type T return obj; } // Example classes that implement the Runnable interface class Runner implements Runnable { run() { console.log("Runner is running"); } } class Swimmer implements Runnable { run() { console.log("Swimmer is swimming"); } } // Using the function with different types const runner = new Runner(); const returnedRunner = processAndReturn(runner); console.log(`Returned object is of type: ${returnedRunner.constructor.name}`); const swimmer = new Swimmer(); const returnedSwimmer = processAndReturn(swimmer); console.log(`Returned object is of type: ${returnedSwimmer.constructor.name}`); ``` If we don't use generics, we will lost the type information. If we try to do it in the javascript level instead of the typescript level, the code will be clumsy. ```ts function processAndReturn(obj: Runnable) { obj.run(); return obj; } // ... Runner and Swimmer classes ... const runner = new Runner(); const returnedRunner = processAndReturn(runner); if (returnedRunner instanceof Runner) { console.log("Returned object is a Runner"); // use Runner methods } const swimmer = new Swimmer(); const returnedSwimmer = processAndReturn(swimmer); if (returnedSwimmer instanceof Swimmer) { console.log("Returned object is a Swimmer"); // use Swimmer methods } ``` > [!info] generics is good at static type inference ## Problem of Generics 1. generics are just type, they are not javascript 2. they can make the code less readable and increase mental overhead ## When to Use Generics ```ts function processAndReturn<T extends Runnable>(obj: T): T { obj.run(); return obj; // this will return T } function processAndReturn(obj: Runnable){ obj.run(); return obj; // this will return Runnable } ``` ==the ultimate reason why typescript generics is useful is that the typescript treat each "block" as a self-contained piece of code that has all the type information.== ==The first example is basically saying that "I know it is some Runnable but I am not sure exactly which Runnable it is" while the second example doesn't have this meaning.== If typescript is smart, then it should know the following is a swimmer because the swimmer type is unknown in static time already. In other words, ==typescript generics is a manual fix to typescript stupidity==. ```ts const swimmer = new Swimmer(); const returnedSwimmer = processAndReturn(swimmer); ``` And that’s why abstract class or other kind of typescript tricks are not the same.