You've learned about types, interfaces, and type aliases. Now let's apply them to functions, the workhorses of any program. Then we'll explore genericsWhat is generics?A way to write code that works with any type of data (numbers, strings, etc.) without duplicating the logic for each type., one of TypeScript's most powerful features.
Typing function parameters and returns
In TypeScript, functions can specify types for their parameters and return values.
// Function declaration with types
function greet(name: string): string {
return `Hello, ${name}!`;
}
// Arrow function with types
const multiply = (a: number, b: number): number => a * b;Explicit return types act as a contract the function must fulfill:
// Without explicit return type - what does this return?
function fetchUser(id: number) {
return database.getUser(id);
}
// With explicit return type - crystal clear
function fetchUser(id: number): Promise<User> {
return database.getUser(id);
}| Syntax | Use Case | Example |
|---|---|---|
function fn(): Type | Named functions | function add(a: number, b: number): number |
const fn = (): Type => | Arrow functions | const add = (a: number, b: number): number => ... |
type Fn = () => Type | Reusable function types | type Handler = (e: Event) => void |
Optional and default parameters
// Optional parameter - might be undefined
function greet(name: string, greeting?: string): string {
const g = greeting || 'Hello';
return `${g}, ${name}!`;
}
// Default parameter - always has a value
function greetDefault(name: string, greeting: string = 'Hello'): string {
return `${greeting}, ${name}!`;
}
greet('Alice'); // "Hello, Alice!"
greetDefault('Bob', 'Hi'); // "Hi, Bob!"Introduction to genericsWhat is generics?A way to write code that works with any type of data (numbers, strings, etc.) without duplicating the logic for each type.
Generics let you write flexible, reusable code that works with any type while maintaining type safety. They're like templates that adapt to whatever data you provide.
// Without generics - use any (loses type safety)
function identity(arg: any): any {
return arg;
}
// With generics - reusable AND type-safe
function identity<T>(arg: T): T {
return arg;
}
const result1 = identity('hello'); // TypeScript infers: string
const result2 = identity(42); // TypeScript infers: numberGeneric constraints
Sometimes you need to limit what types can be used with a generic. Constraints let you specify requirements:
// T must have a 'length' property
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length > b.length ? a : b;
}
longest([1, 2, 3], [1, 2]); // OK - arrays have length
longest('hello', 'world!'); // OK - strings have length
// longest(1, 2); // Error: number doesn't have 'length'The keyof constraint
keyof creates a union of all property keys. Combined with generics, it enables type-safe property access:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'John', age: 30 };
const name = getProperty(user, 'name'); // TypeScript knows: string
const age = getProperty(user, 'age'); // TypeScript knows: number
// getProperty(user, 'email'); // Error: 'email' is not a keyGeneric interfaces and classes
// Generic interface - reusable API response shape
interface APIResponse<T> {
data: T;
status: number;
error: string | null;
}
const userResponse: APIResponse<User> = {
data: { id: 1, name: 'John' },
status: 200,
error: null
};
// Generic class - type-safe data structure
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
}
const nums = new Stack<number>();
nums.push(1);
// nums.push('hello'); // Error: string not assignable to number| Constraint | What it requires | Example |
|---|---|---|
T extends { length: number } | Has length property | Arrays, strings |
T extends object | Is an object (not primitive) | Objects, arrays |
K extends keyof T | K is a key of T | Property names |
T extends U | T is assignable to U | Type inheritance |
Quick reference
| Concept | Syntax | Purpose |
|---|---|---|
| Typed function | function fn(x: number): string | Type-safe parameters and return |
| Optional param | function fn(x?: string) | Parameter can be omitted |
| Default param | function fn(x: string = 'hi') | Fallback value if omitted |
| Generic function | function fn<T>(x: T): T | Works with any type, stays safe |
| Constraint | <T extends HasId> | Limits what types T can be |
| keyof | K extends keyof T | K must be a property name of T |
<T extends object> when the function actually needs <T extends { id: number }>, or <T extends any> which is the same as no constraint at all. Always check that the constraint matches what the function body actually requires, if it accesses .id, the constraint must include id.