TypeScript

Functions in TypeScript

30 min Lesson 8 of 40

Functions in TypeScript

Functions are the building blocks of any TypeScript application. TypeScript extends JavaScript functions with powerful type annotations for parameters, return values, and function signatures. This allows you to catch errors early and write more maintainable code.

Function Type Annotations

The most basic way to add types to functions is by annotating parameters and return types:

Basic Function Types:

// Function with parameter and return type annotations
function add(a: number, b: number): number {
  return a + b;
}

// Arrow function with type annotations
const multiply = (a: number, b: number): number => {
  return a * b;
};

// Shorthand arrow function
const subtract = (a: number, b: number): number => a - b;

// Function with no return value (void)
function logMessage(message: string): void {
  console.log(message);
}

// Usage
const sum = add(5, 3);           // sum: number = 8
const product = multiply(4, 7);  // product: number = 28
logMessage('Hello, TypeScript!'); // Returns void
Note: TypeScript can often infer the return type from the function body, but it's good practice to explicitly annotate return types for better documentation and error detection.

Optional Parameters

Optional parameters allow you to call a function without providing all arguments. Mark a parameter as optional by adding a question mark (?) after the parameter name.

Optional Parameters:

// Function with optional parameter
function greet(name: string, greeting?: string): string {
  if (greeting) {
    return `${greeting}, ${name}!`;
  }
  return `Hello, ${name}!`;
}

// Usage
console.log(greet('Alice'));              // "Hello, Alice!"
console.log(greet('Bob', 'Good morning')); // "Good morning, Bob!"

// Multiple optional parameters
function createUser(
  username: string,
  email?: string,
  age?: number
): object {
  return {
    username,
    email: email || 'N/A',
    age: age || 0
  };
}

const user1 = createUser('john_doe');
const user2 = createUser('jane_smith', 'jane@example.com');
const user3 = createUser('bob_jones', 'bob@example.com', 30);
Warning: Optional parameters must come after required parameters. You cannot have a required parameter after an optional one.

Default Parameters

Default parameters provide a fallback value if no argument is passed. Unlike optional parameters, default parameters don't need the ? marker.

Default Parameters:

// Function with default parameter
function calculatePrice(
  price: number,
  taxRate: number = 0.1,
  discount: number = 0
): number {
  const taxAmount = price * taxRate;
  const discountAmount = price * discount;
  return price + taxAmount - discountAmount;
}

// Usage
console.log(calculatePrice(100));           // Uses defaults: 110
console.log(calculatePrice(100, 0.15));     // Custom tax: 115
console.log(calculatePrice(100, 0.1, 0.2)); // Custom tax and discount: 90

// Default parameters can reference previous parameters
function buildUrl(
  protocol: string = 'https',
  domain: string,
  path: string = '/'
): string {
  return `${protocol}://${domain}${path}`;
}

console.log(buildUrl('http', 'example.com'));
// "http://example.com/"

console.log(buildUrl(undefined, 'example.com', '/about'));
// "https://example.com/about"
Tip: Default parameters are automatically optional. TypeScript infers the parameter type from the default value, so you don't need to specify it explicitly (though you can for clarity).

Rest Parameters

Rest parameters allow a function to accept an indefinite number of arguments as an array. Use the spread operator (...) with a type annotation for the array.

Rest Parameters:

// Function with rest parameters
function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

// Usage
console.log(sum(1, 2, 3));          // 6
console.log(sum(10, 20, 30, 40));   // 100
console.log(sum());                 // 0

// Rest parameters with other parameters
function createTeam(
  teamName: string,
  captain: string,
  ...members: string[]
): object {
  return {
    name: teamName,
    captain,
    members,
    totalMembers: members.length + 1 // +1 for captain
  };
}

const team = createTeam(
  'Alpha Team',
  'John',
  'Alice',
  'Bob',
  'Charlie'
);

console.log(team);
// {
//   name: 'Alpha Team',
//   captain: 'John',
//   members: ['Alice', 'Bob', 'Charlie'],
//   totalMembers: 4
// }

// Rest parameters with different types
function logValues(prefix: string, ...values: (string | number)[]): void {
  values.forEach(value => {
    console.log(`${prefix}: ${value}`);
  });
}

logValues('Item', 'apple', 42, 'banana', 100);
Note: Rest parameters must be the last parameter in the function signature. You can only have one rest parameter per function.

Function Type Expressions

You can define a function type separately and use it to type multiple functions:

Function Type Expressions:

// Define function type
type MathOperation = (a: number, b: number) => number;

// Functions matching the type
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;
const multiply: MathOperation = (a, b) => a * b;
const divide: MathOperation = (a, b) => b !== 0 ? a / b : 0;

// Function that accepts another function
function calculate(
  a: number,
  b: number,
  operation: MathOperation
): number {
  return operation(a, b);
}

// Usage
console.log(calculate(10, 5, add));      // 15
console.log(calculate(10, 5, subtract)); // 5
console.log(calculate(10, 5, multiply)); // 50
console.log(calculate(10, 5, divide));   // 2

// More complex function type
type Validator = (input: string) => {
  isValid: boolean;
  errors: string[];
};

const emailValidator: Validator = (input) => {
  const errors: string[] = [];

  if (!input.includes('@')) {
    errors.push('Email must contain @');
  }

  if (input.length < 5) {
    errors.push('Email too short');
  }

  return {
    isValid: errors.length === 0,
    errors
  };
};

console.log(emailValidator('test@example.com')); // { isValid: true, errors: [] }
console.log(emailValidator('bad'));              // { isValid: false, errors: [...] }

Function Overloads

Function overloads allow you to define multiple function signatures for the same function. This is useful when a function can be called with different parameter types or numbers of parameters.

Function Overloads:

// Overload signatures
function formatDate(date: Date): string;
function formatDate(timestamp: number): string;
function formatDate(year: number, month: number, day: number): string;

// Implementation signature (must be compatible with all overloads)
function formatDate(
  dateOrYear: Date | number,
  month?: number,
  day?: number
): string {
  if (dateOrYear instanceof Date) {
    // Called with Date object
    return dateOrYear.toISOString().split('T')[0];
  } else if (month !== undefined && day !== undefined) {
    // Called with year, month, day
    return `${dateOrYear}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
  } else {
    // Called with timestamp
    return new Date(dateOrYear).toISOString().split('T')[0];
  }
}

// Usage - TypeScript enforces overload signatures
const date1 = formatDate(new Date());         // Date object
const date2 = formatDate(1707926400000);      // Timestamp
const date3 = formatDate(2024, 2, 14);        // Year, month, day

console.log(date1); // "2024-02-14"
console.log(date2); // "2024-02-14"
console.log(date3); // "2024-02-14"

// Another example: search function
function search(query: string): string[];
function search(query: string, limit: number): string[];
function search(query: string, limit: number, offset: number): string[];

function search(
  query: string,
  limit?: number,
  offset?: number
): string[] {
  // Simulate database search
  const allResults = [
    `Result 1 for "${query}"`,
    `Result 2 for "${query}"`,
    `Result 3 for "${query}"`,
    `Result 4 for "${query}"`,
    `Result 5 for "${query}"`
  ];

  const start = offset || 0;
  const end = limit ? start + limit : allResults.length;

  return allResults.slice(start, end);
}

console.log(search('typescript'));        // All results
console.log(search('typescript', 2));     // First 2 results
console.log(search('typescript', 2, 2));  // 2 results starting from index 2
Warning: The implementation signature must be compatible with all overload signatures, but it's not visible to callers. Only the overload signatures are used for type checking.

Generic Functions

Generic functions allow you to write functions that work with multiple types while maintaining type safety:

Generic Functions:

// Generic function with type parameter
function identity<T>(value: T): T {
  return value;
}

// Usage - type is inferred
const num = identity(42);          // T is number
const str = identity('hello');     // T is string
const arr = identity([1, 2, 3]);   // T is number[]

// Generic function with multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const pair1 = pair('age', 30);          // [string, number]
const pair2 = pair(true, 'success');    // [boolean, string]

// Generic function with constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = {
  name: 'Alice',
  age: 30,
  email: 'alice@example.com'
};

const name = getProperty(person, 'name');   // string
const age = getProperty(person, 'age');     // number
// const invalid = getProperty(person, 'invalid'); // Error: invalid key

// Generic array operations
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

function last<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

const numbers = [1, 2, 3, 4, 5];
const firstNum = first(numbers);  // number | undefined
const lastNum = last(numbers);    // number | undefined

const words = ['hello', 'world'];
const firstWord = first(words);   // string | undefined
const lastWord = last(words);     // string | undefined

this Parameter Type

TypeScript allows you to declare the type of this in a function:

this Parameter Type:

interface User {
  name: string;
  age: number;
  greet(this: User): void;
}

const user: User = {
  name: 'Alice',
  age: 30,
  greet() {
    // TypeScript knows 'this' is User
    console.log(`Hello, I'm ${this.name} and I'm ${this.age} years old`);
  }
};

user.greet(); // "Hello, I'm Alice and I'm 30 years old"

// Prevent incorrect this binding
const greetFunction = user.greet;
// greetFunction(); // Error: The 'this' context is not assignable

// Using this in standalone functions
interface Database {
  host: string;
  port: number;
}

function connect(this: Database): string {
  return `Connecting to ${this.host}:${this.port}`;
}

const db: Database = {
  host: 'localhost',
  port: 5432
};

// Call with proper context
console.log(connect.call(db)); // "Connecting to localhost:5432"

Callback Functions

Type callback functions properly to ensure type safety:

Callback Function Types:

// Function accepting callback
function processArray(
  items: number[],
  callback: (item: number, index: number) => void
): void {
  items.forEach((item, index) => {
    callback(item, index);
  });
}

// Usage
processArray([1, 2, 3, 4, 5], (num, idx) => {
  console.log(`Item ${idx}: ${num * 2}`);
});

// Callback with return value
function filterArray<T>(
  items: T[],
  predicate: (item: T) => boolean
): T[] {
  return items.filter(predicate);
}

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = filterArray(numbers, num => num % 2 === 0);
console.log(evenNumbers); // [2, 4, 6, 8, 10]

// Async callback
function fetchData(
  url: string,
  onSuccess: (data: unknown) => void,
  onError: (error: Error) => void
): void {
  fetch(url)
    .then(response => response.json())
    .then(data => onSuccess(data))
    .catch(error => onError(error));
}

fetchData(
  'https://api.example.com/data',
  (data) => console.log('Success:', data),
  (error) => console.error('Error:', error.message)
);

Async Functions

Async functions in TypeScript return a Promise type:

Async Function Types:

// Async function with explicit return type
async function fetchUser(id: string): Promise<{
  id: string;
  name: string;
  email: string;
}> {
  // Simulate API call
  await new Promise(resolve => setTimeout(resolve, 1000));

  return {
    id,
    name: 'John Doe',
    email: 'john@example.com'
  };
}

// Using async function
async function main() {
  try {
    const user = await fetchUser('123');
    console.log(user.name); // TypeScript knows user shape
  } catch (error) {
    console.error('Failed to fetch user:', error);
  }
}

// Generic async function
async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return await response.json() as T;
}

// Usage with type parameter
interface Post {
  id: number;
  title: string;
  body: string;
}

async function loadPost(id: number): Promise<Post> {
  return await fetchData<Post>(`https://api.example.com/posts/${id}`);
}
Exercise:
  1. Create a function calculateDiscount that takes price: number, optional discountPercent: number (default 10), and optional memberDiscount: number. Return the final price after applying discounts.
  2. Create a generic function groupBy that takes an array of items and a key selector function, returns an object grouping items by the selected key.
  3. Create function overloads for getValue that accepts either a string key or a number index to retrieve values from an array or object.
  4. Create an async function fetchUserPosts that takes a user ID and returns a Promise of an array of posts.
  5. Test all your functions with sample data.

Summary

  • Parameter types and return types make functions type-safe and self-documenting
  • Optional parameters use ? and must come after required parameters
  • Default parameters provide fallback values and are automatically optional
  • Rest parameters use ... to accept an indefinite number of arguments as an array
  • Function type expressions allow you to define reusable function types
  • Function overloads define multiple call signatures for the same function
  • Generic functions work with multiple types while maintaining type safety
  • this parameter type ensures correct context binding in functions
  • Async functions return Promise<T> types automatically