Learn how to write flexible and reusable functions with TypeScript's powerful generic functions, enabling you to work with any data type using type parameters.
In our journey through TypeScript, we’ve learned how to define functions that operate on specific data types. However, there are times when we want our functions to be more flexible and work with a variety of types. This is where generic functions come into play. Generics allow us to create functions that can handle different data types without sacrificing type safety. In this section, we’ll explore how to define and use generic functions in TypeScript, providing you with the tools to write more reusable and versatile code.
Generic functions are functions that can operate on any data type. They are defined with a type parameter, which acts as a placeholder for the actual type that will be used when the function is called. This allows us to write functions that are not tied to a specific type, making them more flexible and reusable.
To define a generic function in TypeScript, we use angle brackets (<>
) to specify a type parameter. Let’s start with a simple example: an identity function. An identity function is a function that returns its input without modification.
function identity<T>(arg: T): T {
return arg;
}
// Usage
let output1 = identity<string>("Hello, TypeScript!");
let output2 = identity<number>(42);
console.log(output1); // Output: Hello, TypeScript!
console.log(output2); // Output: 42
In this example, T
is a type parameter that represents the type of the argument arg
. When we call the function, we specify the type we want to use, such as string
or number
. The function then returns a value of the same type.
TypeScript is smart enough to infer the type of the argument in many cases, so we don’t always need to specify the type explicitly. Let’s modify our identity function example to demonstrate this:
let output3 = identity("TypeScript is awesome!");
let output4 = identity(100);
console.log(output3); // Output: TypeScript is awesome!
console.log(output4); // Output: 100
Here, TypeScript infers that output3
is a string
and output4
is a number
based on the arguments passed to the identity
function.
Let’s explore some more examples to solidify our understanding of generic functions.
Suppose we want to create a function that takes a single element and returns an array containing that element. We can use generics to achieve this:
function createArray<T>(element: T, count: number): T[] {
let result: T[] = [];
for (let i = 0; i < count; i++) {
result.push(element);
}
return result;
}
// Usage
let stringArray = createArray<string>("TypeScript", 3);
let numberArray = createArray<number>(7, 5);
console.log(stringArray); // Output: ["TypeScript", "TypeScript", "TypeScript"]
console.log(numberArray); // Output: [7, 7, 7, 7, 7]
In this example, the createArray
function takes an element of type T
and a count, and returns an array of type T
. This function can be used with any type, making it highly reusable.
Sometimes, we want to restrict the types that can be used with a generic function. We can achieve this by using constraints. Constraints allow us to specify that a type parameter must extend a certain type.
extends
Let’s say we want to create a function that works with objects that have a length
property. We can use a constraint to enforce this requirement:
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
// Usage
logLength({ length: 10, value: "Hello" }); // Output: 10
// logLength(3); // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'.
In this example, the logLength
function has a constraint T extends Lengthwise
, which means that T
must be a type that has a length
property. This ensures that we can safely access the length
property within the function.
To practice using generics, let’s convert a non-generic function into a generic one. Consider the following function that concatenates two arrays of numbers:
function concatenateNumbers(arr1: number[], arr2: number[]): number[] {
return arr1.concat(arr2);
}
We can make this function generic so that it can concatenate arrays of any type:
function concatenate<T>(arr1: T[], arr2: T[]): T[] {
return arr1.concat(arr2);
}
// Usage
let combinedArray = concatenate<string>(["Hello"], ["World"]);
console.log(combinedArray); // Output: ["Hello", "World"]
By using a type parameter T
, we allow the concatenate
function to work with arrays of any type, making it more versatile.
Now that we’ve explored generic functions, try converting the following non-generic function into a generic one:
function doubleNumbers(arr: number[]): number[] {
return arr.map((num) => num * 2);
}
Hint: Use a type parameter to allow the function to work with arrays of any type.
To better understand how generic functions work, let’s visualize the process using a flowchart. This flowchart illustrates the steps involved in defining and using a generic function.
flowchart TD A[Define Generic Function] --> B[Specify Type Parameter] B --> C[Write Function Logic] C --> D[Call Function with Specific Type] D --> E[Type Inference or Explicit Type] E --> F[Function Execution]
Caption: This flowchart shows the process of defining and using a generic function in TypeScript. We start by defining the function with a type parameter, write the function logic, call the function with a specific type, and finally execute the function with inferred or explicit types.
For more information on generic functions and other TypeScript features, check out the following resources: