How to Use ForwardRef in TypeScript?

When I first encountered circular reference errors in TypeScript, I was completely stumped. Two classes needed to reference each other, creating a chicken-and-egg problem that TypeScript couldn’t resolve.

This is where ForwardRef comes to the rescue. It’s a pattern that allows you to reference types that haven’t been fully defined yet, solving circular dependency issues elegantly.

In this article, I’ll show you exactly how to implement ForwardRef in TypeScript across different frameworks and scenarios. Let’s dive in!

What is ForwardRef in TypeScript?

ForwardRef is a technique that allows TypeScript to reference a type that will be defined later in your code. This is particularly useful when you have two types that need to reference each other.

Without ForwardRef, you’d get compilation errors because TypeScript can’t resolve the circular dependency during the type checking phase.

Let me show you a simple example of a circular reference problem:

// This won't compile
class Employee {
  manager: Manager;
}

class Manager {
  team: Employee[];
}

In this example, the Employee class refers to Manager, and Manager refers to Employee. TypeScript can’t resolve this circular dependency without some help.

Method 1: Using Interface and Type References

The simplest way to handle forward references in TypeScript is by using interfaces instead of classes for type definitions. Enter the code below in the main.ts file of your TypeScript project.

interface Employee {
  manager: Manager;
  name: string;
}

interface Manager {
  team: Employee[];
  department: string;
}

class EmployeeImpl implements Employee {
  name: string;
  manager!: Manager; // use `!` to say you'll assign it later

  constructor(name: string) {
    this.name = name;
  }
}

class ManagerImpl implements Manager {
  department: string;
  team: Employee[] = [];

  constructor(department: string) {
    this.department = department;
  }

  addEmployee(employee: Employee) {
    this.team.push(employee);
    employee.manager = this;
  }
}

// Create instances and link them
const manager = new ManagerImpl("Engineering");
const emp1 = new EmployeeImpl("Alice");
const emp2 = new EmployeeImpl("Bob");

manager.addEmployee(emp1);
manager.addEmployee(emp2);

// Print out relationships
console.log(manager.team.map(e => e.name)); // ['Alice', 'Bob']
console.log(emp1.manager.department); // 'Engineering'

Now compile and run the TypeScript project using the code in the terminal.

npx tsc main.ts
node main.js

Output:

TypeScript ForwardRef

This approach works because TypeScript handles interface references differently than class references, allowing for forward declarations.

Check out: TypeScript Functions

Method 2: Using forwardRef in React

If you’re working with React, you’ll likely encounter the need for ForwardRef when dealing with component references. React provides a built-in forwardRef function:

import React, { forwardRef, useRef, useImperativeHandle } from 'react';
import './App.css';

// Define the ref type
interface ButtonRefType {
  focus: () => void;
  clickButton: () => void;
}

// Child component that uses forwardRef
const CustomButton = forwardRef<ButtonRefType, { text: string }>((props, ref) => {
  const buttonRef = useRef<HTMLButtonElement>(null);

  useImperativeHandle(ref, () => ({
    focus: () => {
      console.log('[CustomButton] focus() called');
      buttonRef.current?.focus();
    },
    clickButton: () => {
      console.log('[CustomButton] clickButton() called');
      buttonRef.current?.click();
    }
  }));

  console.log('[CustomButton] Rendered');

  return (
    <button
      ref={buttonRef}
      onClick={() => console.log('[CustomButton] Button clicked')}
    >
      {props.text || 'Click me'}
    </button>
  );
});

// Parent component using the ref
function FormComponent() {
  const buttonRef = useRef<ButtonRefType>(null);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log('[FormComponent] handleSubmit triggered');
    buttonRef.current?.clickButton();
  };

  console.log('[FormComponent] Rendered');

  return (
    <form onSubmit={handleSubmit}>
      <CustomButton ref={buttonRef} text="Submit Form" />
      <br />
      <button type="submit">Trigger Custom Button Click</button>
    </form>
  );
}

export default FormComponent;

After this, start the development server using npm start.

Output:

Use forwardref funciton in TypeScript

This pattern is particularly useful for form libraries or when creating accessible component libraries that need to expose DOM methods.

Method 3: Using forwardRef in Angular

Angular has its own implementation of ForwardRef to handle circular dependencies between services or components:

import { forwardRef, Injectable, Inject } from '@angular/core';

@Injectable()
class UserService {
  constructor(@Inject(forwardRef(() => AuthService)) private authService: AuthService) {}

  getUserProfile() {
    if (this.authService.isAuthenticated()) {
      return { name: 'John Doe', state: 'California' };
    }
    return null;
  }
}

@Injectable()
class AuthService {
  constructor(private userService: UserService) {}

  isAuthenticated() {
    return true;
  }

  getUser() {
    return this.userService.getUserProfile();
  }
}

// Module configuration
import { NgModule } from '@angular/core';

@NgModule({
  providers: [
    UserService,
    {
      provide: AuthService,
      useExisting: forwardRef(() => AuthService)
    }
  ]
})
export class AppModule {}

In this example, UserService depends on AuthService and vice versa. Angular’s forwardRef helps resolve this circular dependency.

Check out: TypeScript Global Variables

Method 4: Using Type Assertions for Forward References

Another approach is to use type assertions when you need to reference a type that hasn’t been defined yet:

// forward-ref.ts

class Department {
  manager: Manager;
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  setManager(manager: unknown) {
    // Type assertion to handle forward reference
    this.manager = manager as Manager;
    console.log(`[Department] Manager set to: ${this.manager.name}`);
  }
}

class Manager {
  department: Department;
  name: string;

  constructor(name: string, department: Department) {
    this.name = name;
    this.department = department;
    console.log(`[Manager] Created: ${this.name} for ${this.department.name}`);
  }
}

// Usage
const marketingDept = new Department('Marketing');
const johnDoe = new Manager('John Doe', marketingDept);
marketingDept.setManager(johnDoe);

// Output
console.log(`[Final] ${marketingDept.name} department's manager is ${marketingDept.manager.name}`);

Open a terminal in the folder where your file is and run:

tsc forward-ref.ts

After compilation, execute the code below.

node forward-ref.js

Output:

Forwardref method in typeScript

This approach uses type assertions to tell TypeScript that you know what you’re doing, even if the type isn’t fully defined yet.

Check out: TypeScript Event Types

Method 5: Using typeof for Class References

The typeof operator can also help with forward references, especially when dealing with static members:

class Company {
  departments: typeof Department[];

  addDepartment(dept: typeof Department) {
    // Implementation
  }
}

class Department {
  static createDefault(): Department {
    return new Department('Default');
  }
  constructor(public name: string) {}
}

In this example, typeof Department refers to the class itself, not instances of the class, allowing us to reference it before it’s fully defined.

Check out: Set Default Values in TypeScript Interfaces

Real-World Example: Building a Task Management System

Let’s put everything together with a practical example of a task management system for a company in the United States:

// Forward reference using interfaces
interface IEmployee {
  id: number;
  name: string;
  assignedTasks: ITask[];
  supervisor?: IManager;
}

interface IManager extends IEmployee {
  team: IEmployee[];
  department: string;
}

interface ITask {
  id: number;
  title: string;
  description: string;
  assignee?: IEmployee;
  reviewer?: IManager;
  dueDate: Date;
}

// Implementation classes
class Task implements ITask {
  id: number;
  title: string;
  description: string;
  assignee?: Employee;
  reviewer?: Manager;
  dueDate: Date;

  constructor(id: number, title: string, dueDate: Date) {
    this.id = id;
    this.title = title;
    this.description = '';
    this.dueDate = dueDate;
  }

  assignTo(employee: Employee) {
    this.assignee = employee;
    employee.assignedTasks.push(this);
  }

  setReviewer(manager: Manager) {
    this.reviewer = manager;
  }
}

class Employee implements IEmployee {
  id: number;
  name: string;
  assignedTasks: Task[] = [];
  supervisor?: Manager;

  constructor(id: number, name: string) {
    this.id = id;
    this.name = name;
  }

  assignSupervisor(manager: Manager) {
    this.supervisor = manager;
    manager.addTeamMember(this);
  }
}

class Manager implements IManager {
  id: number;
  name: string;
  assignedTasks: Task[] = [];
  team: Employee[] = [];
  department: string;
  supervisor?: Manager;

  constructor(id: number, name: string, department: string) {
    this.id = id;
    this.name = name;
    this.department = department;
  }

  addTeamMember(employee: Employee) {
    if (!this.team.includes(employee)) {
      this.team.push(employee);
    }
  }
}

// Usage example
const johnDoe = new Employee(1, 'John Doe');
const janeSmith = new Manager(2, 'Jane Smith', 'Engineering');
const projectTask = new Task(101, 'Implement ForwardRef Pattern', new Date('2025-07-15'));

johnDoe.assignSupervisor(janeSmith);
projectTask.assignTo(johnDoe);
projectTask.setReviewer(janeSmith);

console.log(`${johnDoe.name} reports to ${johnDoe.supervisor?.name}`);
console.log(`${johnDoe.name} has ${johnDoe.assignedTasks.length} task(s)`);
console.log(`${janeSmith.name} manages ${janeSmith.team.length} employee(s)`);

Output:

forward ref function in TypeScript

This example demonstrates how interfaces can solve forward reference issues in a real-world application.

Check out: TypeScript Switch Statements

Common Pitfalls and Best Practices

When working with ForwardRef in TypeScript, keep these tips in mind:

  1. Use interfaces over classes when possible for type definitions to avoid circular reference issues.
  2. Don’t overuse type assertions – they bypass TypeScript’s type checking, which can lead to runtime errors.
  3. Keep circular dependencies minimal – while ForwardRef helps resolve them, excessive circular dependencies may indicate design issues.
  4. Framework-specific solutions – Use the built-in ForwardRef implementations in React or Angular when working with those frameworks.
  5. Documentation – Always document when you’re using ForwardRef patterns, as they can be confusing for other developers.

TypeScript’s ForwardRef pattern is a powerful tool that helps you solve circular dependency issues elegantly. By understanding when and how to use it, you can write more maintainable and type-safe code.

Whether you’re building React components that need to forward refs, or dealing with complex service dependencies in Angular, or just managing circular references in your TypeScript classes, ForwardRef has you covered.

I hope this guide helps you navigate the sometimes tricky world of type references in TypeScript. If you have any questions or need further clarification, feel free to ask in the comments below!

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.