Functions are the building blocks of any application, and TypeScript makes them type-safe! You can specify exactly what types of arguments a function accepts and what type it returns. This prevents countless bugs and makes your code self-documenting. Let's master TypeScript function types!
Basic Function Types
In TypeScript, you specify types for function parameters and return values. This ensures functions are called correctly and return expected values.
Function Declaration Syntax
// Basic function with parameter and return types
function greet(name: string): string {
return `Hello, ${name}!`;
}
// Multiple parameters
function add(a: number, b: number): number {
return a + b;
}
// No return value (void)
function logMessage(message: string): void {
console.log(message);
}
// Using the functions
let greeting: string = greet("Yusuf");
let sum: number = add(10, 20);
logMessage("TypeScript is awesome!");
console.log(greeting); // "Hello, Yusuf!"
console.log(sum); // 30
// These cause errors:
// greet(123); // ❌ Error: number not assignable to string
// add("5", "10"); // ❌ Error: string not assignable to number
// let x: number = greet("Hi"); // ❌ Error: string not assignable to numberArrow Function Syntax
// Arrow function with types
const multiply = (a: number, b: number): number => {
return a * b;
};
// Single expression (implicit return)
const square = (n: number): number => n * n;
// No parameters
const getRandomNumber = (): number => {
return Math.random();
};
// Void return
const logError = (error: string): void => {
console.error(error);
};
// Using arrow functions
let product = multiply(5, 4); // 20
let squared = square(7); // 49
let random = getRandomNumber(); // 0.xxx
logError("Something went wrong");
console.log(`Product: ${product}`);
console.log(`Squared: ${squared}`);💡 Return Type Inference
TypeScript can infer return types from the function body, but it's best practice to explicitly type them for clarity and to catch errors early. Always type parameters—they're never inferred!
Optional Parameters
Sometimes function parameters are optional. Use ? to mark parameters as optional. Optional parameters must come after required ones.
// Optional parameter with ?
function greet(name: string, greeting?: string): string {
if (greeting) {
return `${greeting}, ${name}!`;
}
return `Hello, ${name}!`;
}
// Call with or without optional parameter
console.log(greet("Yusuf")); // "Hello, Yusuf!"
console.log(greet("Yusuf", "Hi")); // "Hi, Yusuf!"
console.log(greet("Amina", "Good morning")); // "Good morning, Amina!"
// Multiple optional parameters
function createUser(
name: string,
age?: number,
email?: string
): { name: string; age?: number; email?: string } {
return { name, age, email };
}
let user1 = createUser("Yusuf");
let user2 = createUser("Amina", 30);
let user3 = createUser("Chidi", 28, "chidi@example.com");
console.log(user1); // { name: "Yusuf" }
console.log(user2); // { name: "Amina", age: 30 }
console.log(user3); // { name: "Chidi", age: 28, email: "chidi@example.com" }
// Optional parameters must come last
// function invalid(optional?: string, required: string) {} // ❌ Error!
function valid(required: string, optional?: string) {} // ✅ CorrectHandling Optional Parameters
// Check if optional parameter exists
function displayPrice(price: number, currency?: string): string {
const currencySymbol = currency ?? "₦";
return `${currencySymbol}${price.toFixed(2)}`;
}
console.log(displayPrice(5000)); // "₦5000.00"
console.log(displayPrice(5000, "$")); // "$5000.00"
console.log(displayPrice(5000, "€")); // "€5000.00"
// Using default value pattern
function calculateDiscount(
price: number,
discountPercent?: number
): number {
const discount = discountPercent ?? 0;
return price - (price * discount / 100);
}
console.log(calculateDiscount(10000)); // 10000 (no discount)
console.log(calculateDiscount(10000, 10)); // 9000 (10% off)
console.log(calculateDiscount(10000, 25)); // 7500 (25% off)
// Optional with explicit undefined check
function sendEmail(
to: string,
subject: string,
body?: string
): void {
console.log(`Sending email to: ${to}`);
console.log(`Subject: ${subject}`);
if (body !== undefined) {
console.log(`Body: ${body}`);
} else {
console.log("Body: (empty)");
}
}
sendEmail("user@example.com", "Hello");
sendEmail("user@example.com", "Hello", "This is the message body");Default Parameters
You can provide default values for parameters. If a caller doesn't provide a value, the default is used.
// Default parameters
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
console.log(greet("Yusuf")); // "Hello, Yusuf!"
console.log(greet("Yusuf", "Hi")); // "Hi, Yusuf!"
console.log(greet("Amina", "Good day")); // "Good day, Amina!"
// Multiple default parameters
function createConnection(
host: string,
port: number = 8080,
secure: boolean = false,
timeout: number = 30000
): string {
const protocol = secure ? "https" : "http";
return `${protocol}://${host}:${port} (timeout: ${timeout}ms)`;
}
console.log(createConnection("localhost"));
// "http://localhost:8080 (timeout: 30000ms)"
console.log(createConnection("api.example.com", 443, true));
// "https://api.example.com:443 (timeout: 30000ms)"
console.log(createConnection("localhost", 3000, false, 60000));
// "http://localhost:3000 (timeout: 60000ms)"
// Default parameters with type inference
function multiply(a: number, b: number = 1): number {
return a * b;
}
console.log(multiply(5)); // 5 (5 * 1)
console.log(multiply(5, 3)); // 15 (5 * 3)
// Default can be any expression
function getTimestamp(prefix: string = new Date().toISOString()): string {
return `[${prefix}]`;
}
console.log(getTimestamp()); // "[2024-12-25T10:30:00.000Z]"
console.log(getTimestamp("CUSTOM")); // "[CUSTOM]"Default vs Optional: Default parameters don't need ? because they always have a value. Optional parameters (?) can be undefined, while default parameters are never undefined.
Rest Parameters
Rest parameters allow functions to accept any number of arguments as an array. Use ... before the parameter name.
// Rest parameter - accepts multiple arguments
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20, 30, 40)); // 100
console.log(sum(5)); // 5
console.log(sum()); // 0
// Rest parameter with other parameters
function createMessage(
prefix: string,
...words: string[]
): string {
return prefix + " " + words.join(" ");
}
console.log(createMessage("Hello", "world"));
// "Hello world"
console.log(createMessage("Welcome", "to", "TypeScript", "tutorial"));
// "Welcome to TypeScript tutorial"
// Rest parameter must be last
function logWithTimestamp(
level: string,
...messages: string[]
): void {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}]`, ...messages);
}
logWithTimestamp("INFO", "User logged in");
logWithTimestamp("ERROR", "Database error:", "Connection failed");
// Rest parameters with mixed types
function displayItems(
header: string,
...items: (string | number)[]
): void {
console.log(header);
items.forEach((item, index) => {
console.log(` ${index + 1}. ${item}`);
});
}
displayItems("Shopping List:", "Milk", "Bread", 5, "Eggs", 12);
// Shopping List:
// 1. Milk
// 2. Bread
// 3. 5
// 4. Eggs
// 5. 12Practical Example: Math Utilities
// Find maximum value
function max(...numbers: number[]): number {
if (numbers.length === 0) {
throw new Error("At least one number required");
}
return Math.max(...numbers);
}
// Find minimum value
function min(...numbers: number[]): number {
if (numbers.length === 0) {
throw new Error("At least one number required");
}
return Math.min(...numbers);
}
// Calculate average
function average(...numbers: number[]): number {
if (numbers.length === 0) {
return 0;
}
const total = sum(...numbers);
return total / numbers.length;
}
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
// Usage
console.log(`Max: ${max(5, 10, 3, 8, 15)}`); // 15
console.log(`Min: ${min(5, 10, 3, 8, 15)}`); // 3
console.log(`Average: ${average(10, 20, 30)}`); // 20
console.log(`Sum: ${sum(1, 2, 3, 4, 5)}`); // 15Function Types as Types
You can use function types as type annotations for variables, parameters, and properties.
// Function type annotation
let greet: (name: string) => string;
greet = function(name: string): string {
return `Hello, ${name}!`;
};
console.log(greet("Yusuf")); // "Hello, Yusuf!"
// Function type with multiple parameters
let calculate: (a: number, b: number) => number;
calculate = (a: number, b: number): number => a + b;
console.log(calculate(5, 3)); // 8
calculate = (a: number, b: number): number => a * b;
console.log(calculate(5, 3)); // 15
// Function type as parameter
function processNumbers(
numbers: number[],
operation: (a: number, b: number) => number
): number {
return numbers.reduce(operation);
}
const sum = processNumbers([1, 2, 3, 4, 5], (a, b) => a + b);
const product = processNumbers([1, 2, 3, 4, 5], (a, b) => a * b);
console.log(`Sum: ${sum}`); // 15
console.log(`Product: ${product}`); // 120
// Array of functions
type MathOperation = (a: number, b: number) => number;
const operations: MathOperation[] = [
(a, b) => a + b, // add
(a, b) => a - b, // subtract
(a, b) => a * b, // multiply
(a, b) => a / b // divide
];
console.log(operations[0](10, 5)); // 15
console.log(operations[1](10, 5)); // 5
console.log(operations[2](10, 5)); // 50
console.log(operations[3](10, 5)); // 2Type Aliases for Functions
// Type alias for function type
type GreetFunction = (name: string) => string;
const greet: GreetFunction = (name) => `Hello, ${name}!`;
const welcome: GreetFunction = (name) => `Welcome, ${name}!`;
console.log(greet("Yusuf")); // "Hello, Yusuf!"
console.log(welcome("Amina")); // "Welcome, Amina!"
// Complex function type
type ValidatorFunction = (value: string) => { valid: boolean; error?: string };
const emailValidator: ValidatorFunction = (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailRegex.test(value)) {
return { valid: true };
}
return { valid: false, error: "Invalid email format" };
};
const result1 = emailValidator("yusuf@example.com");
const result2 = emailValidator("invalid-email");
console.log(result1); // { valid: true }
console.log(result2); // { valid: false, error: "Invalid email format" }
// Function type with optional parameters
type LogFunction = (message: string, level?: string) => void;
const logger: LogFunction = (message, level = "INFO") => {
console.log(`[${level}] ${message}`);
};
logger("Application started");
logger("Error occurred", "ERROR");Callback Functions
Callbacks are functions passed as arguments to other functions. Type them properly for safety.
// Function that accepts a callback
function processData(
data: string[],
callback: (item: string, index: number) => void
): void {
data.forEach((item, index) => {
callback(item, index);
});
}
// Using with different callbacks
processData(["Apple", "Banana", "Orange"], (fruit, index) => {
console.log(`${index + 1}. ${fruit}`);
});
// Callback with return value
function filterArray<T>(
array: T[],
predicate: (item: T) => boolean
): T[] {
return array.filter(predicate);
}
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = filterArray(numbers, (num) => num % 2 === 0);
const greaterThanFive = filterArray(numbers, (num) => num > 5);
console.log(`Even numbers: ${evens}`); // [2, 4, 6, 8, 10]
console.log(`Greater than 5: ${greaterThanFive}`); // [6, 7, 8, 9, 10]
// Async callback
function fetchData(
url: string,
onSuccess: (data: any) => void,
onError: (error: Error) => void
): void {
// Simulate API call
setTimeout(() => {
if (url.startsWith("http")) {
onSuccess({ message: "Data fetched successfully" });
} else {
onError(new Error("Invalid URL"));
}
}, 1000);
}
fetchData(
"https://api.example.com/data",
(data) => console.log("Success:", data),
(error) => console.error("Error:", error.message)
);Event Handlers
// Event handler type
type EventHandler = (event: { type: string; timestamp: Date }) => void;
// Event emitter
class EventEmitter {
private handlers: Map<string, EventHandler[]> = new Map();
on(eventType: string, handler: EventHandler): void {
if (!this.handlers.has(eventType)) {
this.handlers.set(eventType, []);
}
this.handlers.get(eventType)!.push(handler);
}
emit(eventType: string): void {
const handlers = this.handlers.get(eventType);
if (handlers) {
const event = { type: eventType, timestamp: new Date() };
handlers.forEach(handler => handler(event));
}
}
}
// Usage
const emitter = new EventEmitter();
emitter.on("userLogin", (event) => {
console.log(`User logged in at ${event.timestamp.toISOString()}`);
});
emitter.on("userLogout", (event) => {
console.log(`User logged out at ${event.timestamp.toISOString()}`);
});
emitter.emit("userLogin");
emitter.emit("userLogout");Function Overloading (Preview)
TypeScript supports function overloading—defining multiple signatures for the same function. We'll cover this in detail in a later lesson.
// Function overload signatures
function formatValue(value: string): string;
function formatValue(value: number): string;
function formatValue(value: boolean): string;
// Implementation signature
function formatValue(value: string | number | boolean): string {
if (typeof value === "string") {
return `"${value}"`;
}
if (typeof value === "number") {
return value.toFixed(2);
}
return value ? "Yes" : "No";
}
console.log(formatValue("Hello")); // ""Hello""
console.log(formatValue(123.456)); // "123.46"
console.log(formatValue(true)); // "Yes"
// We'll learn more about overloading in lesson 13!Practical Examples
Example 1: Form Validator
// Validator function type
type Validator = (value: string) => { valid: boolean; message?: string };
// Create validators
function createMinLengthValidator(minLength: number): Validator {
return (value: string) => {
if (value.length >= minLength) {
return { valid: true };
}
return {
valid: false,
message: `Must be at least ${minLength} characters`
};
};
}
function createEmailValidator(): Validator {
return (value: string) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (regex.test(value)) {
return { valid: true };
}
return { valid: false, message: "Invalid email format" };
};
}
// Validate field
function validateField(
fieldName: string,
value: string,
...validators: Validator[]
): void {
console.log(`Validating ${fieldName}: "${value}"`);
for (const validator of validators) {
const result = validator(value);
if (!result.valid) {
console.log(` ❌ ${result.message}`);
return;
}
}
console.log(" ✅ Valid");
}
// Create validators
const minLength3 = createMinLengthValidator(3);
const minLength8 = createMinLengthValidator(8);
const emailValidator = createEmailValidator();
// Validate form fields
validateField("Username", "ab", minLength3);
validateField("Username", "yusuf", minLength3);
validateField("Password", "pass", minLength8);
validateField("Password", "password123", minLength8);
validateField("Email", "invalid", emailValidator);
validateField("Email", "yusuf@example.com", emailValidator);Example 2: Data Transformer
// Transformer function type
type Transformer<T, R> = (input: T) => R;
// Transform array of items
function transformArray<T, R>(
items: T[],
transformer: Transformer<T, R>
): R[] {
return items.map(transformer);
}
// User data
type User = {
id: number;
name: string;
email: string;
};
type UserSummary = {
id: number;
displayName: string;
};
const users: User[] = [
{ id: 1, name: "Yusuf Ibrahim", email: "yusuf@example.com" },
{ id: 2, name: "Amina Hassan", email: "amina@example.com" },
{ id: 3, name: "Chidi Okafor", email: "chidi@example.com" }
];
// Transform users to summaries
const userToSummary: Transformer<User, UserSummary> = (user) => ({
id: user.id,
displayName: `${user.name} <${user.email}>`
});
const summaries = transformArray(users, userToSummary);
console.log(summaries);
// Transform users to names only
const userToName: Transformer<User, string> = (user) => user.name;
const names = transformArray(users, userToName);
console.log(names); // ["Yusuf Ibrahim", "Amina Hassan", "Chidi Okafor"]
// Transform with calculation
type Product = { name: string; price: number; quantity: number };
const products: Product[] = [
{ name: "Laptop", price: 250000, quantity: 2 },
{ name: "Mouse", price: 5000, quantity: 5 },
{ name: "Keyboard", price: 15000, quantity: 3 }
];
const productToTotal: Transformer<Product, number> = (product) =>
product.price * product.quantity;
const totals = transformArray(products, productToTotal);
console.log(`Totals: ${totals}`); // [500000, 25000, 45000]
console.log(`Grand Total: ₦${totals.reduce((a, b) => a + b, 0)}`);Example 3: Retry Function
// Retry an async operation
async function retry<T>(
operation: () => Promise<T>,
maxAttempts: number = 3,
delay: number = 1000,
onRetry?: (attempt: number, error: Error) => void
): Promise<T> {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
if (onRetry) {
onRetry(attempt, error as Error);
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error("Max attempts reached");
}
// Simulated API call
function fetchData(): Promise<{ data: string }> {
return new Promise((resolve, reject) => {
// Simulate random failure
if (Math.random() < 0.7) {
reject(new Error("Network error"));
} else {
resolve({ data: "Success!" });
}
});
}
// Usage
retry(
fetchData,
5,
2000,
(attempt, error) => {
console.log(`Attempt ${attempt} failed: ${error.message}. Retrying...`);
}
)
.then(result => console.log("Success:", result))
.catch(error => console.error("Final error:", error.message));Test Your Knowledge
Type Safety Check
Which function signature is correct?
// Option A
function greet(name: string) {
return "Hello, " + name;
}
// Option B
function greet(name): string {
return "Hello, " + name;
}
// Option C
function greet(name: string): string {
return "Hello, " + name;
}
// Option D
function greet(name: string): number {
return "Hello, " + name;
}Common TypeScript Error
// Function with type mismatch
function calculateTotal(price: number, quantity: number): string {
return price * quantity; // Error!
}
// Calling with wrong argument types
function greet(name: string): string {
return `Hello, ${name}`;
}
greet(123); // Error!
greet(); // Error!
greet("a", "b"); // Error!❌ Type 'number' is not assignable to type 'string'. Expected 1 argument but got 0 or 2.
What's Wrong?
TypeScript enforces that the return value matches the return type, and that functions are called with the correct number and types of arguments.
// Fixed: Return type matches actual return
function calculateTotal(price: number, quantity: number): number {
return price * quantity; // ✅ Returns number
}
// Fixed: Call with correct arguments
function greet(name: string): string {
return `Hello, ${name}`;
}
greet("Yusuf"); // ✅ Correct type
// Only one argument of type stringSolution: Match return types and provide correct arguments
Best Practices
- Always type parameters—they're never inferred
- Explicitly type return values for clarity and error catching
- Use optional parameters (
?) sparingly - Prefer default parameters over optional when you have a sensible default
- Put required parameters first, then optional/default
- Use rest parameters for variable-length arguments
- Create type aliases for complex function types
- Type callbacks properly to prevent errors
- Use void for functions with no return value
- Document complex function types with comments
// ✅ Good: Clear parameter and return types
function calculateTotal(
price: number,
quantity: number,
discount: number = 0
): number {
const subtotal = price * quantity;
return subtotal - (subtotal * discount / 100);
}
// ✅ Good: Type alias for complex function type
type ValidationFunction = (value: string) => {
valid: boolean;
error?: string;
};
const emailValidator: ValidationFunction = (value) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(value)
? { valid: true }
: { valid: false, error: "Invalid email" };
};
// ✅ Good: Rest parameters for flexibility
function logMessage(level: string, ...messages: string[]): void {
console.log(`[${level}]`, ...messages);
}
// ❌ Avoid: Implicit any parameters
function badExample(value) { // Implicit 'any'
return value;
}
// ❌ Avoid: Optional before required
function alsoBad(optional?: string, required: string) {} // Error!
// ✅ Good: Required first, then optional/default
function goodExample(
required: string,
optional?: string,
withDefault: number = 0
): void {
console.log(required, optional, withDefault);
}Key Takeaways
- Always type function parameters—TypeScript never infers them
- Explicitly type return values for clarity and error prevention
- Optional parameters use
?and must come after required ones - Default parameters provide fallback values automatically
- Rest parameters accept variable numbers of arguments as an array
- Function types can be used as type annotations with
(params) => returnType - Use
voidfor functions that don't return a value - Callbacks should be properly typed to prevent errors
- Type aliases make complex function types reusable
- Arrow functions and regular functions are typed the same way
What's Next?
Excellent work! You've mastered function types and signatures. Next, we'll dive into Interfaces—TypeScript's powerful way to define object shapes and contracts. You'll learn how interfaces differ from type aliases, when to use each, interface extension, implementation, and more. Interfaces are crucial for building scalable TypeScript applications!
Get ready to structure your code with TypeScript interfaces!