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

Functions & Interfaces

Master TypeScript functions with strong typing, overloading, and interfaces for defining object shapes and contracts.

TypeScript Functions & Interfaces

Master TypeScript functions and interfaces with free flashcards and spaced repetition practice. This lesson covers function types, interface declarations, optional and default parameters, generics in functions, and interface extensionsβ€”essential concepts for building type-safe applications in TypeScript.

Welcome to Functions & Interfaces πŸ’»

Functions and interfaces are the building blocks of TypeScript development. While JavaScript gives you functions, TypeScript transforms them into powerful, type-safe constructs that catch errors before runtime. Interfaces define contracts for your code, ensuring objects have the expected shape, while function types guarantee that your functions receive and return the correct data types.

Think of interfaces as blueprints for a building πŸ—οΈβ€”they specify what must exist without dictating how it's built. Functions are the workers that follow these blueprints, performing tasks with predictable inputs and outputs.

Core Concepts

Function Type Annotations πŸ“

Function types in TypeScript specify both parameter types and return types. This ensures that functions are called correctly and return expected values.

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

// Arrow function with types
const multiply = (x: number, y: number): number => x * y;

// Function type as a variable type
let calculate: (a: number, b: number) => number;
calculate = add; // Valid
calculate = multiply; // Valid

The syntax (param: Type) => ReturnType defines a function type signature. This is different from the arrow function syntaxβ€”it's purely a type declaration.

πŸ’‘ Tip: Always annotate return types explicitly, even when TypeScript can infer them. This prevents accidental return type changes when refactoring.

Optional and Default Parameters ❓

Optional parameters are marked with ? and may be undefined. Default parameters provide fallback values.

// Optional parameter
function greet(name: string, title?: string): string {
  if (title) {
    return `Hello, ${title} ${name}`;
  }
  return `Hello, ${name}`;
}

greet("Alice"); // Valid
greet("Bob", "Dr."); // Valid

// Default parameter
function createUser(name: string, role: string = "guest"): object {
  return { name, role };
}

createUser("Charlie"); // { name: "Charlie", role: "guest" }
createUser("Dana", "admin"); // { name: "Dana", role: "admin" }

⚠️ Important: Optional parameters must come after required parameters. Default parameters can appear anywhere but affect the function signature.

Interface Declarations 🎯

Interfaces define the shape of objects. They specify which properties must exist and their types.

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

const user: User = {
  id: 1,
  username: "johndoe",
  email: "john@example.com",
  isActive: true
};

Interfaces can also describe function signatures:

interface MathOperation {
  (a: number, b: number): number;
}

const subtract: MathOperation = (x, y) => x - y;
const divide: MathOperation = (x, y) => x / y;

🧠 Memory Device: "Interface = Intent" - interfaces declare your intent for what an object should contain, not how it's implemented.

Optional Properties in Interfaces πŸ”§

Interfaces can have optional properties using the ? modifier:

interface Product {
  id: number;
  name: string;
  price: number;
  description?: string; // Optional
  inStock?: boolean; // Optional
}

const laptop: Product = {
  id: 101,
  name: "Laptop",
  price: 999
  // description and inStock can be omitted
};

Readonly Properties πŸ”’

The readonly modifier prevents property modification after creation:

interface Config {
  readonly apiKey: string;
  readonly maxRetries: number;
  timeout?: number;
}

const config: Config = {
  apiKey: "abc123",
  maxRetries: 3
};

// config.apiKey = "xyz789"; // Error! Cannot assign to 'apiKey'

Interface Extension 🌳

Interface extension allows interfaces to inherit properties from other interfaces using the extends keyword:

interface Animal {
  name: string;
  age: number;
}

interface Dog extends Animal {
  breed: string;
  bark(): void;
}

const myDog: Dog = {
  name: "Rex",
  age: 5,
  breed: "Golden Retriever",
  bark() {
    console.log("Woof!");
  }
};

You can extend multiple interfaces:

interface Printable {
  print(): void;
}

interface Savable {
  save(): void;
}

interface Document extends Printable, Savable {
  title: string;
  content: string;
}

Function Generics 🎁

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

// Generic function
function identity<T>(arg: T): T {
  return arg;
}

const num = identity<number>(42); // num is number
const str = identity<string>("hello"); // str is string

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

const firstNum = getFirstElement([1, 2, 3]); // number | undefined
const firstStr = getFirstElement(["a", "b"]); // string | undefined

πŸ’‘ Tip: TypeScript can often infer the generic type, so you can call identity(42) without explicitly writing identity<number>(42).

Generic Interfaces πŸ”„

Interfaces can also be generic:

interface Box<T> {
  value: T;
  getValue(): T;
}

const numberBox: Box<number> = {
  value: 100,
  getValue() {
    return this.value;
  }
};

const stringBox: Box<string> = {
  value: "hello",
  getValue() {
    return this.value;
  }
};

Function Overloads πŸ“ž

Function overloading allows multiple function signatures for the same function:

function format(value: string): string;
function format(value: number): string;
function format(value: boolean): string;
function format(value: string | number | boolean): string {
  return String(value);
}

const s1 = format("hello"); // Valid
const s2 = format(42); // Valid
const s3 = format(true); // Valid

The implementation signature (the last one) must be compatible with all overload signatures.

Detailed Examples

Example 1: Building a Type-Safe API Client 🌐

Let's create an interface-based API client for a user management system:

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

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

interface CreateUserRequest {
  name: string;
  email: string;
  password: string;
}

interface ApiClient {
  getUser(id: number): Promise<ApiResponse<User>>;
  createUser(request: CreateUserRequest): Promise<ApiResponse<User>>;
  deleteUser(id: number): Promise<ApiResponse<void>>;
}

class UserApiClient implements ApiClient {
  async getUser(id: number): Promise<ApiResponse<User>> {
    // Simulated API call
    return {
      data: { id, name: "John Doe", email: "john@example.com" },
      status: 200,
      message: "Success"
    };
  }

  async createUser(request: CreateUserRequest): Promise<ApiResponse<User>> {
    // Simulated API call
    return {
      data: { id: 1, name: request.name, email: request.email },
      status: 201,
      message: "User created"
    };
  }

  async deleteUser(id: number): Promise<ApiResponse<void>> {
    return {
      data: undefined as void,
      status: 204,
      message: "User deleted"
    };
  }
}

Why this works: The ApiResponse<T> generic interface allows us to reuse the same response structure for different data types. The ApiClient interface ensures any implementation provides all required methods with correct signatures.

Example 2: Array Utility Functions with Constraints πŸ› οΈ

Generic functions can have type constraints using the extends keyword:

interface Lengthwise {
  length: number;
}

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

logLength("hello"); // Valid - string has length
logLength([1, 2, 3]); // Valid - array has length
logLength({ length: 10, value: "test" }); // Valid - object has length
// logLength(42); // Error! number doesn't have length property

function findMax<T extends { value: number }>(items: T[]): T | null {
  if (items.length === 0) return null;
  
  let max = items[0];
  for (const item of items) {
    if (item.value > max.value) {
      max = item;
    }
  }
  return max;
}

const scores = [
  { value: 85, name: "Alice" },
  { value: 92, name: "Bob" },
  { value: 78, name: "Charlie" }
];

const highest = findMax(scores); // { value: 92, name: "Bob" }

Key concept: The constraint T extends Lengthwise means "T must have at least the properties defined in Lengthwise." This prevents calling the function with incompatible types.

Example 3: Event Handler System πŸŽͺ

A practical example using function types and interfaces:

type EventCallback<T> = (data: T) => void;

interface EventEmitter<T> {
  on(event: string, callback: EventCallback<T>): void;
  emit(event: string, data: T): void;
  off(event: string, callback: EventCallback<T>): void;
}

class SimpleEventEmitter<T> implements EventEmitter<T> {
  private listeners: Map<string, EventCallback<T>[]> = new Map();

  on(event: string, callback: EventCallback<T>): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(callback);
  }

  emit(event: string, data: T): void {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      callbacks.forEach(callback => callback(data));
    }
  }

  off(event: string, callback: EventCallback<T>): void {
    const callbacks = this.listeners.get(event);
    if (callbacks) {
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    }
  }
}

// Usage
interface UserLoginEvent {
  userId: number;
  timestamp: Date;
}

const userEvents = new SimpleEventEmitter<UserLoginEvent>();

userEvents.on("login", (data) => {
  console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});

userEvents.emit("login", { userId: 42, timestamp: new Date() });

What makes this powerful: The generic EventEmitter<T> interface ensures type safetyβ€”you can't emit data of the wrong type for your event system.

Example 4: Database Query Builder πŸ—„οΈ

Combining multiple concepts for a practical use case:

interface QueryBuilder<T> {
  where(field: keyof T, value: any): QueryBuilder<T>;
  orderBy(field: keyof T, direction?: "asc" | "desc"): QueryBuilder<T>;
  limit(count: number): QueryBuilder<T>;
  execute(): Promise<T[]>;
}

interface Product {
  id: number;
  name: string;
  price: number;
  category: string;
}

class ProductQueryBuilder implements QueryBuilder<Product> {
  private conditions: any[] = [];
  private sortField?: keyof Product;
  private sortDirection: "asc" | "desc" = "asc";
  private limitCount?: number;

  where(field: keyof Product, value: any): QueryBuilder<Product> {
    this.conditions.push({ field, value });
    return this; // Method chaining
  }

  orderBy(field: keyof Product, direction: "asc" | "desc" = "asc"): QueryBuilder<Product> {
    this.sortField = field;
    this.sortDirection = direction;
    return this;
  }

  limit(count: number): QueryBuilder<Product> {
    this.limitCount = count;
    return this;
  }

  async execute(): Promise<Product[]> {
    // Simulated database query
    console.log("Executing query with:", {
      conditions: this.conditions,
      sort: this.sortField,
      direction: this.sortDirection,
      limit: this.limitCount
    });
    return [];
  }
}

// Usage with full type safety
const query = new ProductQueryBuilder();
const products = await query
  .where("category", "electronics")
  .where("price", 1000)
  .orderBy("name", "asc")
  .limit(10)
  .execute();

Why keyof T matters: Using keyof Product ensures you can only query by fields that actually exist on the Product interface. TypeScript will catch typos at compile time!

Common Mistakes ⚠️

Mistake 1: Forgetting Optional Parameter Order

// ❌ WRONG - optional before required
function createAccount(username?: string, password: string) {
  // Error! Required parameter cannot follow optional parameter
}

// βœ… CORRECT - optional after required
function createAccount(password: string, username?: string) {
  // Valid
}

Mistake 2: Not Matching Interface Properties Exactly

interface Config {
  apiKey: string;
  timeout: number;
}

// ❌ WRONG - missing required property
const config: Config = {
  apiKey: "abc123"
  // Error! Property 'timeout' is missing
};

// ❌ WRONG - extra property
const config2: Config = {
  apiKey: "abc123",
  timeout: 5000,
  retries: 3 // Error! 'retries' does not exist in type 'Config'
};

// βœ… CORRECT
const config3: Config = {
  apiKey: "abc123",
  timeout: 5000
};

Mistake 3: Modifying Readonly Properties

interface Point {
  readonly x: number;
  readonly y: number;
}

const point: Point = { x: 10, y: 20 };

// ❌ WRONG
point.x = 30; // Error! Cannot assign to 'x' because it is a read-only property

// βœ… CORRECT - create new object
const newPoint: Point = { x: 30, y: point.y };

Mistake 4: Generic Type Inference Confusion

function combine<T>(arr1: T[], arr2: T[]): T[] {
  return [...arr1, ...arr2];
}

// ❌ WRONG - type mismatch
const mixed = combine([1, 2], ["a", "b"]); 
// Error! Type 'string' is not assignable to type 'number'

// βœ… CORRECT - explicit union type
const mixed = combine<number | string>([1, 2], ["a", "b"]);

// βœ… CORRECT - same types
const numbers = combine([1, 2], [3, 4]);

Mistake 5: Missing Implementation Methods

interface Shape {
  area(): number;
  perimeter(): number;
}

// ❌ WRONG - incomplete implementation
class Circle implements Shape {
  constructor(private radius: number) {}
  
  area(): number {
    return Math.PI * this.radius ** 2;
  }
  // Error! Class 'Circle' incorrectly implements interface 'Shape'
  // Property 'perimeter' is missing
}

// βœ… CORRECT
class Circle implements Shape {
  constructor(private radius: number) {}
  
  area(): number {
    return Math.PI * this.radius ** 2;
  }
  
  perimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}

Key Takeaways 🎯

  1. Type annotations on functions prevent runtime errors by catching type mismatches at compile time
  2. Optional parameters (marked with ?) allow flexibility while maintaining type safety
  3. Interfaces define contracts that objects must fulfill, ensuring consistent structure
  4. Readonly properties protect data from accidental modification
  5. Interface extension promotes code reuse through inheritance
  6. Generics enable type-safe, reusable functions and interfaces that work with multiple types
  7. Function overloads provide multiple call signatures for the same function
  8. Type constraints (extends) limit generic types to those with specific properties
  9. Always explicitly annotate return types for better maintainability
  10. Use keyof T to ensure property names are valid at compile time

πŸ€” Did You Know?

TypeScript's interface system uses structural typing (also called "duck typing"), not nominal typing. This means two interfaces with identical properties are considered compatible, even if they have different names. If it looks like a duck and quacks like a duck, TypeScript treats it as a duck! πŸ¦†

πŸ“š Further Study

  1. TypeScript Handbook: Functions - Official documentation on function types and signatures
  2. TypeScript Handbook: Interfaces - Comprehensive guide to interfaces and object types
  3. TypeScript Deep Dive: Generics - Advanced guide to generic types and constraints

πŸ“‹ Quick Reference Card

Function Type(param: Type) => ReturnType
Optional Parameterparam?: Type
Default Parameterparam: Type = defaultValue
Interfaceinterface Name { prop: Type; }
Optional Propertyprop?: Type
Readonly Propertyreadonly prop: Type
Interface Extensioninterface Child extends Parent { }
Generic Functionfunction name<T>(param: T): T { }
Generic Interfaceinterface Name<T> { prop: T; }
Type Constraint<T extends Interface>
keyof Operatorkeyof T - union of property names

Practice Questions

Test your understanding with these questions:

Q1: Write a TypeScript function that takes two parameters of the same generic type and returns that type. The function should be named 'selectFirst'.
A: !AI