From Zero to AI

Lesson 4.1: Functions with Typed Parameters

Duration: 55 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  • Declare functions with typed parameters
  • Use optional parameters with the ? syntax
  • Set default parameter values
  • Work with rest parameters for variable argument lists
  • Understand function overloading basics

Why Type Function Parameters?

In JavaScript, functions accept any arguments without checking:

// JavaScript - no type safety
function greet(name) {
  return 'Hello, ' + name.toUpperCase();
}

greet('Alice'); // Works: "Hello, ALICE"
greet(42); // Crashes at runtime! 42.toUpperCase is not a function
greet(); // Crashes! undefined.toUpperCase is not a function

TypeScript catches these errors before you run the code:

function greet(name: string): string {
  return 'Hello, ' + name.toUpperCase();
}

greet('Alice'); // Works: "Hello, ALICE"
greet(42); // Error: Argument of type 'number' is not assignable to 'string'
greet(); // Error: Expected 1 arguments, but got 0

Basic Function Parameter Types

Single Parameter

Add a type annotation after the parameter name:

function double(value: number) {
  return value * 2;
}

console.log(double(5)); // 10
console.log(double(3.5)); // 7

Multiple Parameters

Each parameter gets its own type:

function createUser(name: string, age: number, isAdmin: boolean) {
  return {
    name: name,
    age: age,
    isAdmin: isAdmin,
  };
}

const user = createUser('Alice', 30, false);
console.log(user); // { name: "Alice", age: 30, isAdmin: false }

Array Parameters

Use the array type syntax:

function sum(numbers: number[]) {
  let total = 0;
  for (const num of numbers) {
    total += num;
  }
  return total;
}

console.log(sum([1, 2, 3, 4, 5])); // 15

Object Parameters

Define the object shape inline or use an interface:

// Inline object type
function printPerson(person: { name: string; age: number }) {
  console.log(`${person.name} is ${person.age} years old`);
}

printPerson({ name: 'Bob', age: 25 });

// Using an interface (cleaner for complex objects)
interface Product {
  id: number;
  name: string;
  price: number;
}

function displayProduct(product: Product) {
  console.log(`${product.name}: $${product.price}`);
}

displayProduct({ id: 1, name: 'Laptop', price: 999 });

Optional Parameters

Sometimes a parameter is not required. Use ? to mark it as optional:

function greet(name: string, greeting?: string) {
  if (greeting) {
    return `${greeting}, ${name}!`;
  }
  return `Hello, ${name}!`;
}

console.log(greet('Alice')); // "Hello, Alice!"
console.log(greet('Alice', 'Welcome')); // "Welcome, Alice!"

Rules for Optional Parameters

  1. Optional parameters must come after required parameters:
// Correct
function example(required: string, optional?: number) {}

// Error: A required parameter cannot follow an optional parameter
function wrong(optional?: string, required: number) {}
  1. Optional parameters are undefined when not provided:
function showInfo(name: string, email?: string) {
  console.log(`Name: ${name}`);
  console.log(`Email: ${email}`); // Could be undefined
}

showInfo('Alice'); // Email: undefined
  1. Check for undefined before using:
function formatName(first: string, last?: string) {
  if (last !== undefined) {
    return `${first} ${last}`;
  }
  return first;
}

console.log(formatName('Alice')); // "Alice"
console.log(formatName('Alice', 'Smith')); // "Alice Smith"

Default Parameters

Default parameters provide a fallback value when the argument is not passed:

function greet(name: string, greeting: string = 'Hello') {
  return `${greeting}, ${name}!`;
}

console.log(greet('Alice')); // "Hello, Alice!"
console.log(greet('Alice', 'Hi')); // "Hi, Alice!"
console.log(greet('Alice', 'Welcome')); // "Welcome, Alice!"

Default Parameters with Types

TypeScript infers the type from the default value:

// TypeScript knows 'count' is a number from the default
function repeat(text: string, count = 1) {
  return text.repeat(count);
}

repeat('ha', 3); // "hahaha"
repeat('ha'); // "ha"
repeat('ha', '3'); // Error: Argument of type 'string' is not assignable to 'number'

Complex Default Values

function createConfig(
  name: string,
  options: { debug: boolean; timeout: number } = { debug: false, timeout: 5000 }
) {
  return { name, ...options };
}

console.log(createConfig('app'));
// { name: "app", debug: false, timeout: 5000 }

console.log(createConfig('app', { debug: true, timeout: 10000 }));
// { name: "app", debug: true, timeout: 10000 }

Rest Parameters

Rest parameters collect multiple arguments into an array:

function sum(...numbers: number[]) {
  let total = 0;
  for (const num of numbers) {
    total += num;
  }
  return total;
}

console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum()); // 0

Rest Parameters with Other Parameters

Rest parameters must be last:

function log(level: string, ...messages: string[]) {
  console.log(`[${level}]`, ...messages);
}

log('INFO', 'Server started');
log('ERROR', 'Connection failed', 'Retrying in 5 seconds');

Typed Rest Parameters

interface LogEntry {
  timestamp: Date;
  message: string;
}

function logAll(...entries: LogEntry[]) {
  for (const entry of entries) {
    console.log(`${entry.timestamp.toISOString()}: ${entry.message}`);
  }
}

logAll(
  { timestamp: new Date(), message: 'Started' },
  { timestamp: new Date(), message: 'Processing' },
  { timestamp: new Date(), message: 'Completed' }
);

Function Overloading

Function overloading lets you define multiple ways to call a function:

// Overload signatures
function combine(a: string, b: string): string;
function combine(a: number, b: number): number;

// Implementation signature
function combine(a: string | number, b: string | number): string | number {
  if (typeof a === 'string' && typeof b === 'string') {
    return a + b;
  }
  if (typeof a === 'number' && typeof b === 'number') {
    return a + b;
  }
  throw new Error('Arguments must be both strings or both numbers');
}

const text = combine('Hello', ' World'); // Type: string
const sum = combine(10, 20); // Type: number

// Error: No overload matches this call
// combine("Hello", 42);

When to Use Overloading

Use overloading when:

  • The return type depends on the input types
  • You want precise types for different argument combinations
// Finding elements - overloaded for single vs multiple results
function find(id: number): User | undefined;
function find(ids: number[]): User[];

function find(idOrIds: number | number[]): User | undefined | User[] {
  if (Array.isArray(idOrIds)) {
    return users.filter((u) => idOrIds.includes(u.id));
  }
  return users.find((u) => u.id === idOrIds);
}

const single = find(1); // Type: User | undefined
const multiple = find([1, 2]); // Type: User[]

Practical Examples

Example 1: Calculator Functions

function add(a: number, b: number) {
  return a + b;
}

function subtract(a: number, b: number) {
  return a - b;
}

function calculate(operation: string, a: number, b: number, precision: number = 2) {
  let result: number;

  switch (operation) {
    case 'add':
      result = a + b;
      break;
    case 'subtract':
      result = a - b;
      break;
    case 'multiply':
      result = a * b;
      break;
    case 'divide':
      result = a / b;
      break;
    default:
      throw new Error(`Unknown operation: ${operation}`);
  }

  return Number(result.toFixed(precision));
}

console.log(calculate('add', 5, 3)); // 8
console.log(calculate('divide', 10, 3)); // 3.33
console.log(calculate('divide', 10, 3, 4)); // 3.3333

Example 2: User Search Function

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

function findUsers(users: User[], searchTerm?: string, role?: string) {
  let results = users;

  if (searchTerm !== undefined) {
    const term = searchTerm.toLowerCase();
    results = results.filter(
      (u) => u.name.toLowerCase().includes(term) || u.email.toLowerCase().includes(term)
    );
  }

  if (role !== undefined) {
    results = results.filter((u) => u.role === role);
  }

  return results;
}

const users: User[] = [
  { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin' },
  { id: 2, name: 'Bob', email: 'bob@example.com', role: 'user' },
  { id: 3, name: 'Charlie', email: 'charlie@example.com', role: 'user' },
];

console.log(findUsers(users)); // All users
console.log(findUsers(users, 'ali')); // Alice
console.log(findUsers(users, undefined, 'user')); // Bob, Charlie
console.log(findUsers(users, 'b', 'user')); // Bob

Example 3: Event Logger

type LogLevel = 'debug' | 'info' | 'warn' | 'error';

function log(level: LogLevel, message: string, ...data: unknown[]) {
  const timestamp = new Date().toISOString();
  const prefix = `[${timestamp}] [${level.toUpperCase()}]`;

  if (data.length > 0) {
    console.log(prefix, message, ...data);
  } else {
    console.log(prefix, message);
  }
}

log('info', 'Application started');
log('debug', 'User data:', { id: 1, name: 'Alice' });
log('error', 'Failed to connect', 'timeout', 5000);

Exercises

Exercise 1: Greeting Function

Create a function personalGreeting that:

  • Takes a required name parameter (string)
  • Takes an optional timeOfDay parameter (string)
  • Returns a greeting like "Good morning, Alice!" or just "Hello, Alice!" if no time provided
Solution
function personalGreeting(name: string, timeOfDay?: string): string {
  if (timeOfDay !== undefined) {
    return `Good ${timeOfDay}, ${name}!`;
  }
  return `Hello, ${name}!`;
}

console.log(personalGreeting('Alice')); // "Hello, Alice!"
console.log(personalGreeting('Bob', 'morning')); // "Good morning, Bob!"
console.log(personalGreeting('Charlie', 'evening')); // "Good evening, Charlie!"

Exercise 2: Array Processor

Create a function processNumbers that:

  • Takes an array of numbers
  • Takes an optional operation parameter with default value "sum"
  • Supports operations: "sum", "average", "max", "min"
  • Returns the result
Solution
function processNumbers(
  numbers: number[],
  operation: 'sum' | 'average' | 'max' | 'min' = 'sum'
): number {
  if (numbers.length === 0) {
    return 0;
  }

  switch (operation) {
    case 'sum':
      return numbers.reduce((a, b) => a + b, 0);
    case 'average':
      return numbers.reduce((a, b) => a + b, 0) / numbers.length;
    case 'max':
      return Math.max(...numbers);
    case 'min':
      return Math.min(...numbers);
  }
}

console.log(processNumbers([1, 2, 3, 4, 5])); // 15
console.log(processNumbers([1, 2, 3, 4, 5], 'average')); // 3
console.log(processNumbers([1, 2, 3, 4, 5], 'max')); // 5
console.log(processNumbers([1, 2, 3, 4, 5], 'min')); // 1

Exercise 3: Contact Builder

Create a function createContact that:

  • Takes required parameters: firstName, lastName, email
  • Takes optional parameters: phone, company
  • Returns a contact object with all provided information
Solution
interface Contact {
  firstName: string;
  lastName: string;
  email: string;
  phone?: string;
  company?: string;
}

function createContact(
  firstName: string,
  lastName: string,
  email: string,
  phone?: string,
  company?: string
): Contact {
  const contact: Contact = {
    firstName,
    lastName,
    email,
  };

  if (phone !== undefined) {
    contact.phone = phone;
  }

  if (company !== undefined) {
    contact.company = company;
  }

  return contact;
}

console.log(createContact('Alice', 'Smith', 'alice@example.com'));
// { firstName: "Alice", lastName: "Smith", email: "alice@example.com" }

console.log(createContact('Bob', 'Jones', 'bob@example.com', '555-1234', 'Acme Inc'));
// { firstName: "Bob", lastName: "Jones", email: "bob@example.com", phone: "555-1234", company: "Acme Inc" }

Exercise 4: String Joiner

Create a function joinStrings that:

  • Uses rest parameters to accept any number of strings
  • Takes an optional separator parameter (default: ", ")
  • Returns all strings joined with the separator
Solution
function joinStrings(separator: string = ', ', ...strings: string[]): string {
  return strings.join(separator);
}

console.log(joinStrings(', ', 'apple', 'banana', 'cherry'));
// "apple, banana, cherry"

console.log(joinStrings(' - ', 'one', 'two', 'three'));
// "one - two - three"

console.log(joinStrings(' | ', 'a', 'b'));
// "a | b"

Key Takeaways

  1. Type parameters with colon: function greet(name: string) defines a typed parameter
  2. Optional parameters use ?: They must come after required parameters
  3. Default parameters provide fallbacks: function greet(name = "World")
  4. Rest parameters collect arguments: function sum(...numbers: number[])
  5. TypeScript catches type errors: Wrong argument types are caught at compile time
  6. Function overloading: Define multiple call signatures for flexible APIs

Resources

Resource Type Description
TypeScript Handbook: Functions Documentation Official guide to functions
TypeScript Handbook: More on Functions Documentation Function overloads and advanced patterns
MDN: Default Parameters Documentation JavaScript default parameters

Next Lesson

Now that you can type function parameters, let us learn how to specify what functions return.

Continue to Lesson 4.2: Return Types