You are viewing a preview of this lesson. Sign in to start learning
Back to TypeScript

TypeScript Functions

Create type-safe functions with parameter typing, return types, and advanced function patterns.

TypeScript Functions

Master TypeScript functions with free flashcards and spaced repetition practice. This lesson covers function declarations, parameter types, return types, arrow functions, optional and default parameters, rest parameters, and function overloadingβ€”essential concepts for building type-safe applications in TypeScript.

Welcome to TypeScript Functions! πŸ’»

Functions are the building blocks of any TypeScript application. Unlike JavaScript, TypeScript adds static type checking to functions, catching errors before runtime and making your code more maintainable and self-documenting. Whether you're defining simple utility functions or complex callback patterns, understanding how to properly type your functions is crucial for leveraging TypeScript's full power.

In this lesson, you'll learn how to write functions that are not only functional but also type-safe, predictable, and easy to maintain. Let's dive into the world of typed functions! πŸš€

Core Concepts πŸ“š

Function Type Annotations

In TypeScript, you can (and should!) annotate both parameters and return types of functions. This provides clarity about what the function expects and what it produces.

Basic Function Declaration:

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

Here:

  • name: string - parameter type annotation
  • : string (after parentheses) - return type annotation

Why type annotations matter:

  • πŸ›‘οΈ Safety: Prevents passing wrong argument types
  • πŸ“– Documentation: Self-documenting code
  • πŸ” IDE Support: Better autocomplete and IntelliSense
  • πŸ› Early Error Detection: Catch mistakes at compile-time

Function Expressions and Arrow Functions

TypeScript supports multiple ways to define functions:

Function Expression:

const add = function(x: number, y: number): number {
  return x + y;
};

Arrow Function:

const multiply = (x: number, y: number): number => {
  return x * y;
};

// Concise body (implicit return)
const divide = (x: number, y: number): number => x / y;

Function Type:

You can also define the type of the function itself:

let calculator: (a: number, b: number) => number;

calculator = (x, y) => x + y; // Valid
calculator = (x, y) => x * y; // Valid
// calculator = (x: string) => x; // ❌ Error!

πŸ’‘ Tip: The function type syntax (param: Type) => ReturnType describes the shape of the function, not its implementation.

Optional and Default Parameters

Optional Parameters:

Use ? to make parameters optional. Optional parameters must come after required ones.

function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return `${firstName} ${lastName}`;
  }
  return firstName;
}

buildName("John"); // Valid: "John"
buildName("John", "Doe"); // Valid: "John Doe"

Default Parameters:

Provide default values for parameters. These are automatically optional.

function createUser(name: string, age: number = 18): string {
  return `${name} is ${age} years old`;
}

createUser("Alice"); // "Alice is 18 years old"
createUser("Bob", 25); // "Bob is 25 years old"

⚠️ Common Mistake: Placing optional parameters before required ones:

// ❌ Wrong:
function wrong(optional?: string, required: number) { }

// βœ… Correct:
function correct(required: number, optional?: string) { }

Rest Parameters

Rest parameters allow functions to accept an indefinite number of arguments as an array.

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

sum(1, 2, 3); // 6
sum(1, 2, 3, 4, 5); // 15

Type annotation for rest parameters: Use an array type (Type[] or Array<Type>).

function logMessages(prefix: string, ...messages: string[]): void {
  messages.forEach(msg => console.log(`${prefix}: ${msg}`));
}

logMessages("INFO", "Server started", "Port 3000", "Ready");

πŸ’‘ Tip: Rest parameters must be the last parameter in the function signature.

Return Type Inference

TypeScript can often infer the return type based on the function body:

// Return type inferred as 'number'
function square(x: number) {
  return x * x;
}

However, explicit return types are recommended for:

  • πŸ“ Public APIs: Makes the contract clear
  • πŸ”’ Preventing errors: Ensures you return what you intend
  • πŸ“š Better documentation: Easier for other developers to understand

Void and Never Return Types

void: Function doesn't return a value (or returns undefined)

function logMessage(message: string): void {
  console.log(message);
  // No return statement, or returns undefined
}

never: Function never returns (throws error or infinite loop)

function throwError(message: string): never {
  throw new Error(message);
  // Execution never reaches the end
}

function infiniteLoop(): never {
  while (true) {
    // Never exits
  }
}

πŸ” Key Difference:

  • void functions complete execution but return nothing useful
  • never functions never complete normal execution

Function Overloading

Function overloading allows you to define multiple function signatures for the same function name:

// Overload signatures
function process(input: string): string;
function process(input: number): number;
function process(input: boolean): string;

// Implementation signature
function process(input: string | number | boolean): string | number {
  if (typeof input === "string") {
    return input.toUpperCase();
  } else if (typeof input === "number") {
    return input * 2;
  } else {
    return input ? "yes" : "no";
  }
}

process("hello"); // Returns string
process(42); // Returns number
process(true); // Returns string

How it works:

  1. Define overload signatures (what callers see)
  2. Define implementation signature (actual code)
  3. Implementation must be compatible with all overloads

⚠️ Important: The implementation signature is not part of the public API. Callers only see the overload signatures.

Detailed Examples πŸ”¬

Example 1: User Registration Function

Let's build a type-safe user registration function with various parameter types:

interface User {
  id: number;
  username: string;
  email: string;
  isActive: boolean;
  createdAt: Date;
}

function registerUser(
  username: string,
  email: string,
  isActive: boolean = true
): User {
  return {
    id: Math.floor(Math.random() * 10000),
    username,
    email,
    isActive,
    createdAt: new Date()
  };
}

// Usage:
const user1 = registerUser("johndoe", "john@example.com");
const user2 = registerUser("janedoe", "jane@example.com", false);

Key points:

  • βœ… Required parameters: username, email
  • βœ… Default parameter: isActive defaults to true
  • βœ… Return type: Explicitly typed as User interface
  • βœ… Type safety: Can't pass wrong types or miss required params

Example 2: Callback Functions with Types

TypeScript excels at typing callback functions, common in async operations:

type ProcessCallback = (result: string) => void;
type ErrorCallback = (error: Error) => void;

function fetchData(
  url: string,
  onSuccess: ProcessCallback,
  onError: ErrorCallback
): void {
  // Simulated async operation
  setTimeout(() => {
    const success = Math.random() > 0.5;
    
    if (success) {
      onSuccess(`Data from ${url}`);
    } else {
      onError(new Error("Failed to fetch data"));
    }
  }, 1000);
}

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

Why this is powerful:

  • 🎯 Type aliases make callback signatures reusable
  • πŸ›‘οΈ TypeScript ensures callbacks have correct signatures
  • πŸ“ IDE autocomplete helps with callback parameters

Example 3: Generic Functions

Generics allow functions to work with multiple types while maintaining type safety:

function getFirstElement<T>(array: T[]): T | undefined {
  return array[0];
}

// TypeScript infers the type parameter
const firstNumber = getFirstElement([1, 2, 3]); // Type: number | undefined
const firstName = getFirstElement(["Alice", "Bob"]); // Type: string | undefined
const firstUser = getFirstElement([user1, user2]); // Type: User | undefined

Generic with constraints:

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(`Length: ${item.length}`);
}

logLength("hello"); // Valid: string has length
logLength([1, 2, 3]); // Valid: array has length
logLength({ length: 5, data: "test" }); // Valid: object has length
// logLength(42); // ❌ Error: number doesn't have length

Why generics matter:

  • πŸ”„ Reusability without sacrificing type safety
  • 🎯 Types are preserved through the function
  • 🧩 Constraints ensure minimum requirements

Example 4: Function Overloading for Flexible APIs

Overloading creates flexible, intuitive APIs:

// Overload signatures
function createElement(tag: "img"): HTMLImageElement;
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: string): HTMLElement;

// Implementation
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

// TypeScript knows the exact return type!
const img = createElement("img"); // Type: HTMLImageElement
img.src = "photo.jpg"; // βœ… Valid: HTMLImageElement has 'src'

const input = createElement("input"); // Type: HTMLInputElement
input.value = "test"; // βœ… Valid: HTMLInputElement has 'value'

const div = createElement("div"); // Type: HTMLDivElement

Real-world benefit: Different overloads return precise types, giving you better autocomplete and type checking.

Common Mistakes ⚠️

1. Forgetting Return Type Annotations

❌ Wrong:

function calculate(x: number, y: number) {
  // Return type inferred, but not explicit
  return x + y;
}

βœ… Correct:

function calculate(x: number, y: number): number {
  return x + y;
}

Why it matters: Explicit return types catch errors where you accidentally return the wrong type.

2. Misplacing Optional Parameters

❌ Wrong:

function format(prefix?: string, text: string): string {
  return prefix ? `${prefix}: ${text}` : text;
}

βœ… Correct:

function format(text: string, prefix?: string): string {
  return prefix ? `${prefix}: ${text}` : text;
}

3. Not Checking for undefined with Optional Parameters

❌ Wrong:

function greet(name?: string): string {
  return `Hello, ${name.toUpperCase()}!`; // ❌ name might be undefined!
}

βœ… Correct:

function greet(name?: string): string {
  return `Hello, ${name?.toUpperCase() ?? "Guest"}!`;
}

4. Incorrect Function Type Syntax

❌ Wrong:

// Using colon instead of arrow
let myFunc: (x: number, y: number): number;

βœ… Correct:

let myFunc: (x: number, y: number) => number;

5. Overload Implementation Too Restrictive

❌ Wrong:

function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
// Implementation too narrow!
function combine(a: string, b: string): string {
  return a + b;
}

βœ… Correct:

function combine(a: string, b: string): string;
function combine(a: number, b: number): number;
// Implementation covers all overloads
function combine(a: string | number, b: string | number): string | number {
  return (a as any) + (b as any);
}

6. Confusing void and undefined

❌ Wrong:

function log(message: string): undefined {
  console.log(message);
  return undefined; // Unnecessary explicit return
}

βœ… Correct:

function log(message: string): void {
  console.log(message);
  // No return needed
}

πŸ’‘ Remember: Use void for functions that don't return useful values, even if they technically return undefined.

Key Takeaways 🎯

  1. Always annotate parameters and return types for clarity and safety
  2. Use optional parameters (?) and default parameters to create flexible APIs
  3. Rest parameters (...args) collect multiple arguments into an array
  4. Function overloading provides multiple signatures for the same function
  5. void means no useful return value; never means never returns
  6. Generic functions (<T>) enable type-safe reusable code
  7. Arrow functions and function expressions can be fully typed
  8. Optional parameters must come after required ones
  9. Check for undefined when using optional parameters
  10. Explicit return types prevent accidental type errors

πŸ“‹ Quick Reference Card

Syntax Example Usage
Basic function function add(x: number, y: number): number Standard function declaration
Arrow function const add = (x: number, y: number): number => x + y Concise function expression
Optional parameter function greet(name?: string): string Parameter can be omitted
Default parameter function greet(name: string = "Guest"): string Parameter has default value
Rest parameters function sum(...nums: number[]): number Accept variable arguments
Function type let calc: (x: number, y: number) => number Type of a function variable
Generic function function first<T>(arr: T[]): T Work with multiple types
void return function log(msg: string): void No useful return value
never return function error(msg: string): never Never completes normally
Overloading function process(x: string): string;
function process(x: number): number;
Multiple signatures

πŸ“š Further Study

  1. TypeScript Handbook - Functions: https://www.typescriptlang.org/docs/handbook/2/functions.html
  2. TypeScript Deep Dive - Functions: https://basarat.gitbook.io/typescript/type-system/functions
  3. MDN - Functions: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions

Congratulations! πŸŽ‰ You now understand how to write type-safe functions in TypeScript. Practice by adding type annotations to your existing JavaScript functions, and explore function overloading and generics to build more flexible APIs. The more you type your functions, the more errors you'll catch early and the better your development experience will become! πŸ’»βœ¨