Ever used a vending machine? Money goes in, you push a button, a specific thing comes out. The rules are dead simple. It takes coins and bills (the input types) and hands back snacks or drinks (the output type). Try feeding it a rock and it spits the rock right back at you.
Typed functions work the same way. You decide EXACTLY what goes in and what comes out, and TypeScript rejects anything that doesn't fit.
Typing Function Parameters
Every function parameter should get a type annotation.
Example
// Each parameter has a type
function greet(name: string, age: number): string {
return `Hello, ${name}! You are ${age} years old.`;
}
greet("Alice", 28); // OK
greet("Bob", "thirty"); // Error! 'string' is not assignable to 'number'
greet("Charlie"); // Error! Expected 2 arguments, but got 1
Optional and Default Parameters
Example
// Optional parameter (use ?)
function createUser(name: string, email?: string): void {
console.log(`Name: ${name}`);
if (email) {
console.log(`Email: ${email}`);
}
}
createUser("Alice"); // OK
createUser("Bob", "bob@example.com"); // OK
// Default parameter (provides a fallback value)
function greetUser(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
greetUser("Alice"); // "Hello, Alice!"
greetUser("Bob", "Hey"); // "Hey, Bob!"
Return Types
The return type goes right after the parameter list, with a colon in front of it.
Example
// Explicitly typed return
function add(a: number, b: number): number {
return a + b;
}
// TypeScript catches wrong return types
function isAdult(age: number): boolean {
return age >= 18; // OK - returns boolean
}
// void means the function returns nothing
function logMessage(message: string): void {
console.log(message);
// No return statement needed
}
Arrow Functions with Types
Arrow functions follow pretty much the same pattern.
Example
// Typed arrow function
const multiply = (x: number, y: number): number => {
return x * y;
};
// Short form for simple returns
const double = (n: number): number => n * 2;
// Function type annotation (variable holds a function)
const formatPrice: (amount: number) => string = (amount) => {
return `$${amount.toFixed(2)}`;
};
Function Types
You can even describe the shape of a whole function with a type.
Example
// Define a function type
type MathOperation = (a: number, b: number) => number;
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
const multiply: MathOperation = (a, b) => a * b;
// Use function types as parameters
function calculate(x: number, y: number, operation: MathOperation): number {
return operation(x, y);
}
calculate(10, 5, add); // 15
calculate(10, 5, subtract); // 5
Introduction to Generics
Okay, deep breath. Generics have a reputation for being the scary part, and they're honestly one of the best features TypeScript has. They let you write functions and types that work with any type while keeping all your type safety.
Picture a box that can hold anything. The trick is that once you put something in it, the box REMEMBERS what's inside.
Example
// Without generics - you lose type information
function getFirst(items: any[]): any {
return items[0];
}
const first = getFirst([1, 2, 3]); // type is 'any' - not helpful
// With generics - type is preserved
function getFirstItem<T>(items: T[]): T {
return items[0];
}
const firstNumber = getFirstItem([1, 2, 3]); // type is 'number'
const firstString = getFirstItem(["a", "b", "c"]); // type is 'string'
That <T> is a type parameter. It's a placeholder that gets filled in when you call the function... like a blank on a form that TypeScript fills in based on whatever you pass it.
Generic Functions in Practice
Example
// A function that wraps any value in an array
function wrapInArray<T>(item: T): T[] {
return [item];
}
wrapInArray(42); // returns number[] = [42]
wrapInArray("hello"); // returns string[] = ["hello"]
// Multiple type parameters
function makePair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const pair = makePair("age", 25); // type is [string, number]
Generic Constraints
Sometimes you want a generic with some guardrails on it. Constraints make sure the type parameter has certain properties.
Example
// Constraint: T must have a 'length' property
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
getLength("hello"); // OK - strings have length
getLength([1, 2, 3]); // OK - arrays have length
getLength(42); // Error! number doesn't have 'length'
// Constraint with interface
interface HasId {
id: number;
}
function findById<T extends HasId>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
Real-World Example: API Helper
Here's where generics really earn their keep in a real app.
Example
interface ApiResponse<T> {
data: T | null;
error: string | null;
loading: boolean;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
try {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data: T = await response.json();
return { data, error: null, loading: false };
} catch (err) {
return { data: null, error: "Failed to fetch", loading: false };
}
}
// TypeScript knows the exact shape of the response
interface User {
id: number;
name: string;
}
const result = await fetchData<User>("/api/user/1");
// Check for a failed request before reading the user
if (result.data) {
console.log(result.data.name); // TypeScript knows 'name' exists here
} else {
console.error(result.error);
}
TL;DR
Function parameters need type annotations, like function greet(name: string)
Return types go after the parameters, like function add(a: number, b: number): number
Use ? for optional parameters and = value for defaults
Generics (<T>) let you write type-safe functions that work with any type
Constraints (<T extends Something>) put limits on which types a generic will take
Arrow functions get typed the same way regular functions do
What's Next?
With functions and generics in your back pocket, you can write code that's flexible AND type-safe. Next up is union types, which let a variable be one of a few different types, and type narrowing, which helps TypeScript figure out which one you're actually holding...