- Published on
Part 3: Functions and Generics
- Authors
- Name
- Diego Herrera Redondo
- @diegxherrera
Welcome back to the TypeScript Crash Course! 🎉 In Part 2, we covered interfaces and type aliases for defining complex data structures. In Part 3, we’ll dive into functions and generics—two essential aspects of TypeScript that enable us to write reusable, type-safe code. Generics, in particular, add flexibility to our code, making it adaptable for different data types while maintaining type safety. Let’s get started! 🚀
Function Types in TypeScript 🛠️
In TypeScript, we can specify the types for function parameters and return values, helping catch errors early and making functions more predictable.
Example: Basic Function Type Annotations
function greet(name: string): string {
return `Hello, ${name}!`;
}
console.log(greet("Alice")); // Output: Hello, Alice!
// greet(123); // ❌ Error: Argument of type 'number' is not assignable to parameter of type 'string'.
Optional and Default Parameters
TypeScript allows optional parameters (using ?
) and default values for function parameters.
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
console.log(greet("Alice")); // Output: Hello, Alice!
console.log(greet("Bob", "Hi")); // Output: Hi, Bob!
In this example, greeting
is optional with a default value of "Hello"
.
Function Types and Call Signatures 🔍
We can use function types to define the signature of a function, which is particularly useful when passing functions as parameters.
Example: Function Type
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (x, y) => x + y;
const multiply: MathOperation = (x, y) => x * y;
console.log(add(5, 3)); // Output: 8
console.log(multiply(5, 3)); // Output: 15
Here, MathOperation
defines the type of a function that takes two numbers and returns a number.
Introduction to Generics 📦
Generics allow us to define functions, interfaces, or classes that work with any data type. They’re a powerful feature in TypeScript for creating reusable and flexible components.
Basic Syntax of Generics
In TypeScript, generics are defined with angle brackets (<T>
) where T
represents a type variable. You can name this type variable anything, but T
is a common convention.
Example: Generic Function
function identity<T>(value: T): T {
return value;
}
console.log(identity<string>("Hello")); // Output: Hello
console.log(identity<number>(42)); // Output: 42
In this example, identity
takes a parameter of type T
and returns a value of the same type T
. When calling identity
, we specify the type (<string>
or <number>
), making it adaptable to different types.
Working with Generic Constraints 🔒
Sometimes, you want to restrict the types that can be used with generics. Constraints allow us to specify that a generic type must adhere to certain conditions.
Example: Using Constraints
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
logLength("Hello"); // Output: 5 (string has a length property)
logLength([1, 2, 3]); // Output: 3 (array has a length property)
// logLength(42); // ❌ Error: Argument of type 'number' is not assignable to parameter of type 'HasLength'.
In this example, logLength
only accepts types that have a length
property (like string
and array
). This way, TypeScript ensures that item.length
is valid.
Generic Interfaces and Classes 🧩
Generics can also be applied to interfaces and classes, allowing for more flexible data structures.
Generic Interface Example
interface Box<T> {
contents: T;
}
let stringBox: Box<string> = { contents: "Hello" };
let numberBox: Box<number> = { contents: 123 };
console.log(stringBox.contents); // Output: Hello
console.log(numberBox.contents); // Output: 123
In this example, Box<T>
is a generic interface that stores a value of type T
. We create stringBox
and numberBox
using different types.
Generic Class Example
class Pair<T, U> {
constructor(public first: T, public second: U) {}
}
let pair = new Pair("Alice", 30);
console.log(pair.first); // Output: Alice
console.log(pair.second); // Output: 30
Here, Pair<T, U>
is a generic class that holds two properties of potentially different types.
Practical Example: Data Wrapper 📦
Let’s put generics to practical use by creating a data wrapper function that can wrap any type of data.
- Define a generic interface
Wrapper<T>
with a single property,value
, of typeT
. - Write a function
wrap
that takes a value of typeT
and returns aWrapper<T>
.
Example Solution
interface Wrapper<T> {
value: T;
}
function wrap<T>(value: T): Wrapper<T> {
return { value };
}
let wrappedString = wrap("Hello, TypeScript");
let wrappedNumber = wrap(100);
console.log(wrappedString); // Output: { value: 'Hello, TypeScript' }
console.log(wrappedNumber); // Output: { value: 100 }
In this example, wrap
creates a generic wrapper around any value, making it adaptable for different types.
Practice Challenge: Flexible Storage 🎲
Let’s practice with a flexible storage class.
- Create a generic class
Storage<T>
that can store a single value of any type. - Add
getItem
andsetItem
methods to retrieve and set the value. - Test the class with different types.
Example Solution
class Storage<T> {
private item: T;
constructor(item: T) {
this.item = item;
}
getItem(): T {
return this.item;
}
setItem(item: T): void {
this.item = item;
}
}
const stringStorage = new Storage<string>("Initial String");
console.log(stringStorage.getItem()); // Output: Initial String
stringStorage.setItem("Updated String");
console.log(stringStorage.getItem()); // Output: Updated String
const numberStorage = new Storage<number>(42);
console.log(numberStorage.getItem()); // Output: 42
In this example, Storage<T>
can store and manage values of different types, providing a flexible and reusable storage solution.
Wrapping Up
In Part 3 of our TypeScript series, we explored function types and generics, two powerful tools for writing reusable and type-safe code. Generics give your code flexibility, allowing you to work with multiple types while maintaining TypeScript’s type-checking benefits.
In Part 4, we’ll explore type assertions, unions, and intersections to gain even more control over type management in TypeScript. Thanks for following along, and happy coding! 🎉