Functions#
Functions are the building blocks of any application. TypeScript adds type safety to function parameters and return values.
Function Type Annotations#
Basic Function Types#
// Named function with types
function add(x: number, y: number): number {
return x + y;
}
// Arrow function with types
const multiply = (x: number, y: number): number => {
return x * y;
};
// Short arrow function
const subtract = (x: number, y: number): number => x - y;
// Function with no return value
function logMessage(message: string): void {
console.log(message);
}
Type Inference on Return Types#
// TypeScript can infer the return type
function divide(x: number, y: number) {
return x / y; // Inferred as number
}
// But explicit is often better for documentation
function getUser(id: string): User {
// Return type makes intent clear
return { id, name: "John" };
}
Optional and Default Parameters#
Optional Parameters#
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return `${firstName} ${lastName}`;
}
return firstName;
}
console.log(buildName("John")); // "John"
console.log(buildName("John", "Doe")); // "John Doe"
// Optional parameters must come after required ones
function greet(greeting?: string, name: string): string { // ❌ Error
return `${greeting} ${name}`;
}
Default Parameters#
function createUser(
name: string,
role: string = "user",
isActive: boolean = true
): User {
return { name, role, isActive };
}
createUser("Alice"); // role: "user", isActive: true
createUser("Bob", "admin"); // role: "admin", isActive: true
createUser("Charlie", "user", false); // role: "user", isActive: false
// Default parameters can use complex expressions
function greet(name: string, greeting: string = getDefaultGreeting()): string {
return `${greeting}, ${name}!`;
}
Rest Parameters#
// Rest parameters are arrays
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
// Rest parameters with other parameters
function multiply(multiplier: number, ...numbers: number[]): number[] {
return numbers.map(n => n * multiplier);
}
console.log(multiply(2, 1, 2, 3)); // [2, 4, 6]
// Typed tuple rest parameters
function createUser(
name: string,
...roles: [string, ...string[]]
): User {
return { name, roles };
}
createUser("Alice", "admin"); // At least one role required
createUser("Bob", "user", "moderator"); // Multiple roles OK
Function Overloads#
Define multiple function signatures for the same function:
// Overload signatures
function getValue(id: number): string;
function getValue(name: string): number;
// Implementation signature
function getValue(idOrName: number | string): string | number {
if (typeof idOrName === "number") {
return `ID: ${idOrName}`;
} else {
return idOrName.length;
}
}
const result1 = getValue(42); // Type: string
const result2 = getValue("Alice"); // Type: number
// Real-world example: createElement
function createElement(tag: "img"): HTMLImageElement;
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
const img = createElement("img"); // Type: HTMLImageElement
const input = createElement("input"); // Type: HTMLInputElement
const div = createElement("div"); // Type: HTMLElement
Function Types#
Function Type Expressions#
// Function type
type MathOperation = (x: number, y: number) => number;
const add: MathOperation = (x, y) => x + y;
const subtract: MathOperation = (x, y) => x - y;
// Using in other types
type Calculator = {
operation: MathOperation;
name: string;
};
// Callback function type
type Callback = (error: Error | null, data: any) => void;
function fetchData(url: string, callback: Callback): void {
// ...
}
Call Signatures#
// More detailed function types
type DescribableFunction = {
description: string;
(value: number): boolean; // Call signature
};
function doSomething(fn: DescribableFunction): void {
console.log(fn.description + " returned " + fn(6));
}
function isEven(value: number): boolean {
return value % 2 === 0;
}
isEven.description = "Check if number is even";
doSomething(isEven);
Construct Signatures#
// Constructor function type
type Constructor = {
new (name: string): { name: string };
};
function createInstance(Ctor: Constructor, name: string) {
return new Ctor(name);
}
class Person {
constructor(public name: string) {}
}
const person = createInstance(Person, "Alice");
Generic Functions#
Write reusable functions that work with multiple types:
// Basic generic function
function identity<T>(arg: T): T {
return arg;
}
const num = identity<number>(42); // Type: number
const str = identity<string>("hello"); // Type: string
const auto = identity(true); // Type inference: boolean
// Generic with constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "Alice", age: 25 };
const name = getProperty(person, "name"); // Type: string
const age = getProperty(person, "age"); // Type: number
// getProperty(person, "invalid"); // ❌ Error
// Multiple type parameters
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const result = merge({ name: "Alice" }, { age: 25 });
// result has type: { name: string } & { age: number }
// Generic with default type
function createArray<T = string>(length: number, value: T): T[] {
return Array(length).fill(value);
}
const strings = createArray(3, "hello"); // Type: string[]
const numbers = createArray<number>(3, 0); // Type: number[]
Arrow Functions#
// Basic arrow function
const greet = (name: string): string => `Hello, ${name}!`;
// Multi-line arrow function
const processUser = (user: User): string => {
const fullName = `${user.firstName} ${user.lastName}`;
return fullName.toUpperCase();
};
// Arrow function as callback
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((n: number): number => n * 2);
// this context preservation
class Counter {
count = 0;
// Arrow function preserves 'this'
increment = (): void => {
this.count++;
};
// Regular function - 'this' can change
decrement(): void {
this.count--;
}
}
Async Functions#
// Async function returns Promise
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return data;
}
// Using the async function
async function displayUser(id: string): Promise<void> {
try {
const user = await fetchUser(id);
console.log(user);
} catch (error) {
console.error("Failed to fetch user:", error);
}
}
// Arrow async function
const fetchData = async (url: string): Promise<any> => {
const response = await fetch(url);
return response.json();
};
// Generic async function
async function fetchItems<T>(url: string): Promise<T[]> {
const response = await fetch(url);
return response.json();
}
const users = await fetchItems<User>("/api/users");
const products = await fetchItems<Product>("/api/products");
Callback Functions#
// Simple callback
function processArray(
arr: number[],
callback: (item: number) => number
): number[] {
return arr.map(callback);
}
processArray([1, 2, 3], n => n * 2); // [2, 4, 6]
// Node.js style callbacks
type NodeCallback<T> = (error: Error | null, data?: T) => void;
function readFile(path: string, callback: NodeCallback<string>): void {
// Simulated async operation
setTimeout(() => {
if (path) {
callback(null, "file contents");
} else {
callback(new Error("Invalid path"));
}
}, 100);
}
// Event handler callback
type EventHandler = (event: MouseEvent) => void;
function addClickListener(element: HTMLElement, handler: EventHandler): void {
element.addEventListener("click", handler);
}
Function Type Guards#
// Type predicate
function isString(value: unknown): value is string {
return typeof value === "string";
}
function processValue(value: string | number): void {
if (isString(value)) {
console.log(value.toUpperCase()); // TypeScript knows it's a string
} else {
console.log(value.toFixed(2)); // TypeScript knows it's a number
}
}
// Custom type guard for objects
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird): void {
if (isFish(pet)) {
pet.swim(); // TypeScript knows it's Fish
} else {
pet.fly(); // TypeScript knows it's Bird
}
}
Practical Examples#
API Request Handler#
type ApiResponse<T> = {
data: T | null;
error: string | null;
status: number;
};
async function apiRequest<T>(
url: string,
options?: RequestInit
): Promise<ApiResponse<T>> {
try {
const response = await fetch(url, options);
const data = await response.json();
return {
data,
error: null,
status: response.status
};
} catch (error) {
return {
data: null,
error: error instanceof Error ? error.message : "Unknown error",
status: 500
};
}
}
// Usage
const result = await apiRequest<User>("/api/user/123");
if (result.error) {
console.error(result.error);
} else {
console.log(result.data);
}
Event Emitter#
type EventHandler<T = any> = (data: T) => void;
class EventEmitter<Events extends Record<string, any>> {
private listeners: Map<keyof Events, EventHandler[]> = new Map();
on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void {
const handlers = this.listeners.get(event) || [];
handlers.push(handler);
this.listeners.set(event, handlers);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
const handlers = this.listeners.get(event) || [];
handlers.forEach(handler => handler(data));
}
}
// Usage
type AppEvents = {
userLogin: { userId: string; timestamp: number };
userLogout: { userId: string };
error: { message: string; code: number };
};
const emitter = new EventEmitter<AppEvents>();
emitter.on("userLogin", (data) => {
console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});
emitter.emit("userLogin", { userId: "123", timestamp: Date.now() });
Validation Function#
type ValidationRule<T> = (value: T) => string | null;
function createValidator<T>(
rules: ValidationRule<T>[]
): (value: T) => string[] {
return (value: T) => {
const errors: string[] = [];
for (const rule of rules) {
const error = rule(value);
if (error) {
errors.push(error);
}
}
return errors;
};
}
// Usage
const validatePassword = createValidator<string>([
(pwd) => pwd.length < 8 ? "Password must be at least 8 characters" : null,
(pwd) => !/[A-Z]/.test(pwd) ? "Password must contain uppercase letter" : null,
(pwd) => !/[0-9]/.test(pwd) ? "Password must contain a number" : null,
]);
const errors = validatePassword("weak");
console.log(errors); // ["Password must be at least 8 characters", ...]
Best Practices#
-
✅ Always type function parameters
function greet(name: string): string { /* ... */ } -
✅ Return type can often be inferred, but explicit is clearer
function getUser(id: string): Promise<User> { /* ... */ } -
✅ Use optional parameters instead of undefined unions
function buildName(first: string, last?: string) { /* ... */ } // Instead of: (first: string, last: string | undefined) -
✅ Use function overloads for truly different behaviors
function process(x: number): string; function process(x: string): number; -
✅ Prefer type predicates for type guards
function isString(x: unknown): x is string { /* ... */ }
Next Steps#
- Learn about Interfaces & Types
- Explore Generics in depth
- Master Type Guards & Narrowing