🏗️Functions and Type Behavior

Understand how to type functions, use callbacks, and handle advanced parameter patterns.

🧠 Function Types and Callbacks

In TypeScript, functions can be typed to ensure they receive the correct inputs and return expected outputs. Properly typing functions makes code more maintainable and reduces errors.

💡 Function Types Explained

  • Parameter types: Define expected input types for each function argument
  • Return type: Specify the type of value the function returns
  • Optional parameters: Mark arguments that are not required
  • Default values: Provide fallback values for missing arguments
function greet(name: string, age?: number): string {
    return `Hello ${name}! You are ${age ?? 'unknown'} years old.`;
}

greet('Alice'); // Works
console.log(greet('Bob', 30));

Optional and Default Parameters

Use the optional operator (?) to mark parameters that are not required. You can also provide default values using =.

interface User {
    name: string;
    age?: number;
}

function createUser(name: string, age = 18): User {
    return { name, age };
}

const user = createUser('Charlie');
console.log(user.age); // 18

💡 Higher-Order Functions

Functions that accept other functions as arguments or return new functions are called higher-order functions. They're essential for creating reusable and flexible code.

function createLogger(level: string) {
    return function(message: string, context: string = 'default') {
        console.log(`${level}: ${message} (context: ${context})`);
    };
}

const debugLogger = createLogger('DEBUG');
debugLogger('Something happened');

Callbacks in Practice

Callbacks allow functions to °handle asynchronous operations§. They're widely used in Node.js and NestJS for operations like database queries.

function processRequest(request: any, callback: (result: string, error?: Error) => void) {
    try {
        const result = `Processed ${request}`;
        callback(result);
    } catch (error) {
        callback(null, error);
    }
}

processRequest('data', (result, error) => {
    if (error) {
        console.error('Error:', error);
    } else {
        console.log('Success:', result);
    }
});

💡 Best Practices for Function Types

  • Always define parameter types to ensure correct input values
  • Use optional parameters sparingly and document their usage
  • Type callback functions to ensure they receive expected arguments
  • Consider using Promises or Async/Await for cleaner asynchronous code
async function processRequest(request: any) {
    try {
        return `Processed ${request}`;
    } catch (error) {
        throw new Error(`Processing failed: ${error.message}`);
    }
}

processRequest('data').then(result =>
    console.log('Success:', result)
).catch(error =>
    console.error('Error:', error)
);

🔁 Overloads and Rest Parameters

Function overloading is a powerful TypeScript feature that allows you to define multiple implementations of the same function with different parameters. This enables your functions to accept various argument signatures while maintaining type safety and clarity.

💡 Introduction to Function Overloads

When working with variadic functions, you often need to handle different numbers of arguments. TypeScript's rest parameters (denoted by `...args`) allow you to capture multiple arguments into an array while preserving type information.

Defining Function Overloads

function print(message: string): void;
function print(message: string, options: { verbose?: boolean }): void;

function print(message: string, options?: { verbose?: boolean }) {
  if (options?.verbose) {
    console.log(` VERBOSE: ${message}`);
  } else {
    console.log(message);
  }
}

Important: Overload signatures must be placed before the implementation signature and should not include a function body.

Using Rest Parameters

function sum(...numbers: number[]): number {
  return numbers.reduce((acc, num) => acc + num, 0);
}

sum(1, 2, 3); // 6
sum(5);       // 5
  • Rest parameters create an array of the provided arguments.
  • You can combine rest parameters with other types by placing them last in the parameter list.
  • Use `T[]` for variadic functions where T is the expected type.

💡 Advanced Overload Scenarios

function merge(a: string, b: string): string;
function merge(a: any[], b: any[]): any[];

function merge(a: string | any[], b: string | any[]) {
  if (typeof a === 'string') {
    return a + b;
  }
  return [...a, ...b];
}

This example demonstrates how to handle both string concatenation and array merging with a single function.

Best Practices for Overloads

  • Always define the implementation signature last.
  • Keep overload signatures simple and focused on different argument patterns.
  • Use precise types in both signatures to avoid confusion.

Common Pitfalls

  • Don't place the implementation signature before overloads.
  • Avoid using overly complex parameter types in overload signatures.
  • Never use `any` when specific types can be used.

💡 Real-World Applications

function validate(value: string, rules: { required: boolean }): string;
function validate(value: string, rules: { pattern: RegExp }): string;

function validate(value: string, rules: { required?: boolean; pattern?: RegExp }) {
  if (rules.required && !value) return 'Field is required';
  if (rules.pattern && !rules.pattern.test(value)) return 'Invalid format';
  return value;
}

This validation function handles both presence checks and regex patterns depending on the rules provided.

💡 Summary

  • Function overloads provide flexible argument handling while maintaining type safety.
  • Rest parameters enable variadic functions with proper typing.
  • Always test edge cases when working with complex overloads.

⚠️ Void, Never, and Unknown

In this chapter, we'll explore three essential TypeScript types that help you write safer and more maintainable code: void, never, and unknown. These might seem rare at first glance, but they're crucial for handling specific scenarios in your applications.

💡 Understanding Void Types

The void type is used to explicitly indicate that a function does not return any value. While TypeScript can infer this automatically, using void makes your intentions clear to other developers.

function logMessage(message: string): void {
  console.log(message);
}

// You can also omit the return type since TypeScript will default to void
function logMessageWithoutReturnType(message: string) {
  console.log(message);
}

💡 Key Points About Void

  • void is used for functions that don't return a value.
  • Explicitly declaring void improves code readability.
  • Avoid using void when you actually need to return a value.

💡 The Never Type

The never type represents functions that never complete normally. This could be because they throw an error or enter an infinite loop.

function handleError(message: string): never {
  throw new Error(message);
}

// Example of a function that will never return
while (true) {
  console.log('This will run forever');
}

💡 Key Points About Never

  • never is used for functions that throw errors or loop indefinitely.
  • Use never to explicitly communicate that a function doesn't return normally.
  • Combine never with TypeScript's type guards to improve error handling.

💡 The Unknown Type

The unknown type is a safer alternative to any. It represents values whose types are not known at compile time but must be checked before use.

function handleData(data: unknown): void {
  if (typeof data === 'string') {
    console.log('Data is a string:', data);
  } else if (Array.isArray(data)) {
    console.log('Data is an array with length:', data.length);
  }
}

💡 Key Points About Unknown

  • unknown replaces any when you need to work with uncertain types.
  • Always check the type of an unknown value before using it.
  • Use TypeScript's type guards (typeof, Array.isArray, etc.) to safely narrow down types.

💡 Best Practices for Using Void, Never, and Unknown

  • Use void when a function's primary purpose is side effects.
  • Reserve never for functions that intentionally do not return.
  • Prefer unknown over any to enforce type safety.
  • Combine unknown with type checks to ensure correct handling.

Common Mistakes to Avoid

  • Don't use void when you actually need to return a value.
  • Avoid using any when unknown would provide better type safety.
  • Don't assume TypeScript will automatically handle type checks for unknown values.

💡 Real-World Applications

Understanding these types is essential for building robust applications with NestJS. For example, when creating API endpoints that may receive malformed data, using unknown ensures you properly validate inputs before processing them.

Quiz

Question 1 of 15

Which syntax defines a function parameter as optional?

  • function fn(param: string)
  • function fn(param?: string)
  • function fn([param]: string)
  • function fn(...param: string)