Introduction: What are Generics and why are they important?
\n\nWhen learning TypeScript, you quickly encounter a situation where you need to write a function or class that can work with different data types. Without special tools, you would have to either duplicate code for each type or use any, which kills all the magic of strict typing. This is where Generics come to the rescue.
Generics are a powerful TypeScript mechanism that allows you to create components (functions, classes, interfaces) that work with different types while maintaining strict typing. Imagine you are writing a universal container that can store apples, books, and numbers, but TypeScript knows exactly what is inside in each specific case.
\n\nIn this article, we will break down why Generics are needed, how to declare them, and how to use them in real projects. You will learn how to make your code more flexible, safe, and reusable.
\n\nGenerics Basics: syntax and your first function
\n\nLet's start with a simple example. Suppose we need a function that takes an argument and returns it (identity function). Without generics, we would write it like this:
\n\nfunction identity(arg: number): number {\n return arg;\n}\n\nBut this function only works with numbers. To make it universal, we can use any:
function identity(arg: any): any {\n return arg;\n}\n\nThe problem is that we lose type information. TypeScript doesn't know that the same type that was passed will be returned. This is where Generics come in:
\n\nfunction identity<T>(arg: T): T {\n return arg;\n}\n\n// Usage:\nlet output1 = identity<string>("Hello"); // type string\nlet output2 = identity<number>(42); // type number\n\nLet's break down the syntax:
\n- \n
<T>— this is a type parameter. You can use any letter, but by convention,T(Type),U,Vare most often used. \n arg: T— the argument has typeT. \n : Tafter the parentheses — the function returns a value of typeT. \n
Now TypeScript knows: if you passed a string, the output will be a string. If a number — a number. Type inference often works automatically, so you can just write identity("Hello") — TypeScript will understand that T is string.
Generics in functions: working with arrays and constraints
\n\nMost often, generics are used for working with arrays or collections. Consider a function that returns the first element of an array:
\n\nfunction getFirst<T>(arr: T[]): T | undefined {\n return arr[0];\n}\n\nconst numbers = [1, 2, 3];\nconst firstNumber = getFirst(numbers); // type number\n\nconst strings = ["a", "b", "c"];\nconst firstString = getFirst(strings); // type string\n\nNote: T[] is shorthand for Array<T>. The function works with any array, but returns an element of exactly the type stored in the array.
Constraints using extends
\n\nSometimes you need the type T to have certain properties. For example, we want to get the length of an object. Not all types have a length property, so we can impose a constraint:
interface HasLength {\n length: number;\n}\n\nfunction getLength<T extends HasLength>(arg: T): number {\n return arg.length;\n}\n\ngetLength("string"); // OK, string has length\ngetLength([1, 2, 3]); // OK, array has length\ngetLength(42); // Error! Number does not have length