TypeScript Tutorial: Creating a Simple Web-Based Code Diff Tool

In the world of software development, change is the only constant. Code evolves, features are added, and bugs are squashed. Tracking these changes, understanding what’s been modified, and ensuring the integrity of the codebase are crucial tasks. This is where code diff tools come into play. They highlight the differences between two versions of a file, making it easy to see exactly what’s changed. This tutorial will guide you through building a simple, yet functional, web-based code diff tool using TypeScript. We’ll explore the core concepts, implement the necessary logic, and create a user-friendly interface. By the end of this tutorial, you’ll have a practical understanding of how code diffing works and a tool you can use to compare code snippets.

Why Build a Code Diff Tool?

While many excellent code diff tools are available, building your own offers several benefits:

  • Learning: It’s an excellent way to deepen your understanding of algorithms, string manipulation, and UI design.
  • Customization: You can tailor the tool to your specific needs and preferences.
  • Portability: A web-based tool can be accessed from any device with a browser.
  • Practicality: It’s a valuable skill to have, and the tool itself can be surprisingly useful in your daily development workflow.

Imagine you’re reviewing a pull request, and you need to quickly understand the changes. Or perhaps you’re troubleshooting a bug and want to pinpoint the exact line of code that introduced the issue. A code diff tool simplifies these tasks, saving you time and effort.

Core Concepts: Understanding Code Diffing

At its heart, code diffing involves comparing two strings (representing the code) and identifying the differences. Several algorithms can accomplish this, with varying levels of complexity and efficiency. We’ll use a relatively straightforward approach based on the Myers diff algorithm, known for its balance of speed and accuracy.

The basic steps involved are:

  1. Line-by-Line Comparison: The algorithm compares the two code snippets line by line.
  2. Identifying Changes: It identifies lines that are added, removed, or modified.
  3. Highlighting Differences: The tool then highlights these changes, typically using colors (e.g., green for additions, red for removals) to make them visually distinct.

Before we dive into the code, let’s understand the different types of changes a diff tool needs to handle:

  • Addition: A line of code present in the new version but not the old.
  • Deletion: A line of code present in the old version but not the new.
  • Modification: A line of code that has been changed.
  • No Change: A line of code that is the same in both versions.

Setting Up Your TypeScript Project

Let’s get started by setting up our TypeScript project. We’ll use a simple HTML file to display the results and some basic CSS for styling. You’ll also need Node.js and npm (or yarn) installed on your system.

  1. Create a Project Directory: Create a new directory for your project (e.g., `code-diff-tool`).
  2. Initialize npm: Navigate to the project directory in your terminal and run `npm init -y`. This creates a `package.json` file.
  3. Install TypeScript: Install TypeScript as a development dependency: `npm install typescript –save-dev`.
  4. Create `tsconfig.json`: Run `npx tsc –init` to generate a `tsconfig.json` file. This file configures the TypeScript compiler. You can customize it as needed, but the default settings will work for this tutorial.
  5. Create `index.html`: Create an `index.html` file in your project directory with the following content:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Code Diff Tool</title>
    <style>
        .diff-container {
            display: flex;
            font-family: monospace;
        }

        .code-column {
            width: 50%;
            padding: 10px;
            border: 1px solid #ccc;
            overflow-x: auto;
        }

        .diff-line {
            display: flex;
            align-items: center;
            padding: 2px 0;
        }

        .line-number {
            width: 30px;
            text-align: right;
            margin-right: 10px;
            color: #999;
        }

        .added {
            background-color: #e6ffed;
            color: #429158;
        }

        .removed {
            background-color: #fddddd;
            color: #c0392b;
        }

        .unchanged {
            color: #222;
        }
    </style>
</head>
<body>
    <div class="diff-container">
        <div class="code-column">
            <h2>Old Code</h2>
            <div id="old-code"></div>
        </div>
        <div class="code-column">
            <h2>New Code</h2>
            <div id="new-code"></div>
        </div>
    </div>
    <script src="./index.js"></script>
</body>
</html>

This HTML provides the basic structure for our tool. It includes two columns to display the old and new code, and some basic CSS for styling. It also links to an `index.js` file, which we’ll create later.

  1. Create `index.ts`: Create an `index.ts` file in your project directory. This is where our TypeScript code will reside. For now, let’s add a simple `console.log` statement to ensure everything is set up correctly:
console.log("Hello, Code Diff Tool!");
  1. Compile TypeScript: In your terminal, run `npx tsc` to compile the TypeScript code into JavaScript. This will create an `index.js` file.
  2. Open `index.html` in your browser: Open the `index.html` file in your browser. You should see “Hello, Code Diff Tool!” in your browser’s developer console (usually accessed by pressing F12).

Implementing the Code Diff Logic

Now, let’s implement the core diffing logic. We’ll create a function that takes two strings (the old and new code) and returns an array of objects, each representing a line of code with its status (added, removed, or unchanged).

Here’s the TypeScript code for the diffing function. This function uses a simplified version of the Myers diff algorithm, focusing on line-by-line comparisons for clarity:


interface DiffResult {
  line: string;
  status: 'added' | 'removed' | 'unchanged';
}

function diff(oldCode: string, newCode: string): DiffResult[] {
  const oldLines = oldCode.split('n');
  const newLines = newCode.split('n');
  const diffResult: DiffResult[] = [];

  let i = 0;
  let j = 0;

  while (i < oldLines.length || j < newLines.length) {
    if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
      diffResult.push({ line: oldLines[i], status: 'unchanged' });
      i++;
      j++;
    } else if (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {
      diffResult.push({ line: oldLines[i], status: 'removed' });
      i++;
    } else if (j < newLines.length && (i >= oldLines.length || oldLines[i] !== newLines[j])) {
      diffResult.push({ line: newLines[j], status: 'added' });
      j++;
    }
  }

  return diffResult;
}

Let’s break down this code:

  • `DiffResult` Interface: Defines the structure of the result objects, specifying a `line` (the code line) and a `status` (whether it’s added, removed, or unchanged).
  • `diff` Function: This is the main function. It takes two strings (`oldCode` and `newCode`) as input and returns an array of `DiffResult` objects.
  • Splitting into Lines: The code splits both the `oldCode` and `newCode` into arrays of lines using the newline character (`n`) as a delimiter.
  • Iterating and Comparing: The code uses two index variables, `i` and `j`, to iterate through the `oldLines` and `newLines` arrays.
  • Comparison Logic:
    • If the lines at the current indices `i` and `j` are equal, it means the line is unchanged.
    • If a line exists in `oldCode` but not in `newCode`, it’s considered removed.
    • If a line exists in `newCode` but not in `oldCode`, it’s considered added.
  • Building the `diffResult` Array: The function pushes a `DiffResult` object for each line, indicating its line of code and its status.
  • Returning the Result: The function returns the `diffResult` array.

Integrating with the HTML and Displaying the Results

Now, let’s integrate our diffing function with the HTML to display the results in the browser.

Replace the content of your `index.ts` file with the following code:


interface DiffResult {
  line: string;
  status: 'added' | 'removed' | 'unchanged';
}

function diff(oldCode: string, newCode: string): DiffResult[] {
  const oldLines = oldCode.split('n');
  const newLines = newCode.split('n');
  const diffResult: DiffResult[] = [];

  let i = 0;
  let j = 0;

  while (i < oldLines.length || j < newLines.length) {
    if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
      diffResult.push({ line: oldLines[i], status: 'unchanged' });
      i++;
      j++;
    } else if (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {
      diffResult.push({ line: oldLines[i], status: 'removed' });
      i++;
    } else if (j < newLines.length && (i >= oldLines.length || oldLines[i] !== newLines[j])) {
      diffResult.push({ line: newLines[j], status: 'added' });
      j++;
    }
  }

  return diffResult;
}

// Sample code snippets (replace with your own)
const oldCode = `function add(a, b) {
  return a + b;
}

console.log(add(2, 3));`;

const newCode = `function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

console.log(add(2, 3));
console.log(subtract(5, 2));`;

// Get the DOM elements
const oldCodeElement = document.getElementById('old-code');
const newCodeElement = document.getElementById('new-code');

// Perform the diff
const diffResult = diff(oldCode, newCode);

// Function to render the diff results
function renderDiff(diffResult: DiffResult[], element: HTMLElement) {
  const lines = diffResult.map((result, index) => {
    const lineNumber = index + 1;
    const lineElement = document.createElement('div');
    lineElement.classList.add('diff-line');

    const lineNumberElement = document.createElement('span');
    lineNumberElement.classList.add('line-number');
    lineNumberElement.textContent = lineNumber.toString();
    lineElement.appendChild(lineNumberElement);

    const codeSpan = document.createElement('span');
    codeSpan.textContent = result.line;

    if (result.status === 'added') {
      lineElement.classList.add('added');
    } else if (result.status === 'removed') {
      lineElement.classList.add('removed');
    } else {
      lineElement.classList.add('unchanged');
    }

    lineElement.appendChild(codeSpan);
    return lineElement;
  });

  lines.forEach(lineElement => element.appendChild(lineElement));
}

// Render the diff results
if (oldCodeElement && newCodeElement) {
  renderDiff(diffResult, oldCodeElement);
  renderDiff(diffResult, newCodeElement);
}

Let’s break down the added parts:

  • Sample Code Snippets: We define two sample code snippets, `oldCode` and `newCode`. Replace these with your own code snippets to test the tool.
  • DOM Element Retrieval: We get references to the `old-code` and `new-code` divs in the HTML using `document.getElementById()`.
  • Calling the `diff` function: We call the `diff` function with the `oldCode` and `newCode` snippets.
  • `renderDiff` Function: This function takes the `diffResult` and the DOM element (e.g., `oldCodeElement`) as input. It iterates through the `diffResult` and creates HTML elements for each line of code. It also adds CSS classes (`added`, `removed`, `unchanged`) to highlight the changes.
  • Rendering the Results: The code calls `renderDiff` for both the `oldCodeElement` and `newCodeElement` to display the diff results in the respective columns.

Now, compile your TypeScript code (`npx tsc`) and refresh your `index.html` in the browser. You should see the two code snippets side-by-side, with the differences highlighted.

Enhancements and Features

Our basic code diff tool is functional, but we can enhance it with several features to make it more user-friendly and powerful.

  • User Input: Allow users to paste or type in the code snippets directly into text areas.
  • Syntax Highlighting: Use a library like Prism.js or highlight.js to add syntax highlighting to the code snippets.
  • Line Numbers: Display line numbers for easier navigation and reference.
  • Collapsed View: Allow users to collapse unchanged lines to focus on the changes.
  • File Upload: Enable users to upload code files for comparison.
  • More Sophisticated Diffing Algorithm: Implement a more advanced algorithm (e.g., Myers diff algorithm) for improved accuracy and handling of complex changes.

Let’s implement the user input feature to get you started.

  1. Add Textareas to HTML: Modify your `index.html` to include textareas for user input:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Code Diff Tool</title>
    <style>
        .diff-container {
            display: flex;
            font-family: monospace;
        }

        .code-column {
            width: 50%;
            padding: 10px;
            border: 1px solid #ccc;
            overflow-x: auto;
        }

        .diff-line {
            display: flex;
            align-items: center;
            padding: 2px 0;
        }

        .line-number {
            width: 30px;
            text-align: right;
            margin-right: 10px;
            color: #999;
        }

        .added {
            background-color: #e6ffed;
            color: #429158;
        }

        .removed {
            background-color: #fddddd;
            color: #c0392b;
        }

        .unchanged {
            color: #222;
        }

        .textarea-container {
            margin-bottom: 10px;
        }

        textarea {
            width: 100%;
            height: 150px;
            font-family: monospace;
            padding: 5px;
            border: 1px solid #ccc;
            resize: vertical;
        }
    </style>
</head>
<body>
    <div class="diff-container">
        <div class="code-column">
            <div class="textarea-container">
                <textarea id="old-code-input" placeholder="Enter Old Code"></textarea>
            </div>
            <h2>Old Code</h2>
            <div id="old-code"></div>
        </div>
        <div class="code-column">
            <div class="textarea-container">
                <textarea id="new-code-input" placeholder="Enter New Code"></textarea>
            </div>
            <h2>New Code</h2>
            <div id="new-code"></div>
        </div>
    </div>
    <script src="./index.js"></script>
</body>
</html>

We’ve added two textareas with the IDs `old-code-input` and `new-code-input`. We also added a `textarea-container` class for styling.

  1. Update TypeScript Code: Modify your `index.ts` to get the code from the textareas and trigger the diffing process.

interface DiffResult {
  line: string;
  status: 'added' | 'removed' | 'unchanged';
}

function diff(oldCode: string, newCode: string): DiffResult[] {
  const oldLines = oldCode.split('n');
  const newLines = newCode.split('n');
  const diffResult: DiffResult[] = [];

  let i = 0;
  let j = 0;

  while (i < oldLines.length || j < newLines.length) {
    if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
      diffResult.push({ line: oldLines[i], status: 'unchanged' });
      i++;
      j++;
    } else if (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {
      diffResult.push({ line: oldLines[i], status: 'removed' });
      i++;
    } else if (j < newLines.length && (i >= oldLines.length || oldLines[i] !== newLines[j])) {
      diffResult.push({ line: newLines[j], status: 'added' });
      j++;
    }
  }

  return diffResult;
}

// Get the DOM elements
const oldCodeElement = document.getElementById('old-code') as HTMLElement;
const newCodeElement = document.getElementById('new-code') as HTMLElement;
const oldCodeInput = document.getElementById('old-code-input') as HTMLTextAreaElement;
const newCodeInput = document.getElementById('new-code-input') as HTMLTextAreaElement;

// Function to render the diff results
function renderDiff(diffResult: DiffResult[], element: HTMLElement) {
    element.innerHTML = ''; // Clear previous results
  const lines = diffResult.map((result, index) => {
    const lineNumber = index + 1;
    const lineElement = document.createElement('div');
    lineElement.classList.add('diff-line');

    const lineNumberElement = document.createElement('span');
    lineNumberElement.classList.add('line-number');
    lineNumberElement.textContent = lineNumber.toString();
    lineElement.appendChild(lineNumberElement);

    const codeSpan = document.createElement('span');
    codeSpan.textContent = result.line;

    if (result.status === 'added') {
      lineElement.classList.add('added');
    } else if (result.status === 'removed') {
      lineElement.classList.add('removed');
    } else {
      lineElement.classList.add('unchanged');
    }

    lineElement.appendChild(codeSpan);
    return lineElement;
  });

  lines.forEach(lineElement => element.appendChild(lineElement));
}

// Function to perform the diff and render the results
function performDiff() {
    if (!oldCodeInput || !newCodeInput || !oldCodeElement || !newCodeElement) {
        console.error('One or more required elements not found.');
        return;
    }

    const oldCode = oldCodeInput.value;
    const newCode = newCodeInput.value;
    const diffResult = diff(oldCode, newCode);

    renderDiff(diffResult, oldCodeElement);
    renderDiff(diffResult, newCodeElement);
}

// Add event listeners to the textareas
if (oldCodeInput && newCodeInput) {
  oldCodeInput.addEventListener('input', performDiff);
  newCodeInput.addEventListener('input', performDiff);
}

Key changes:

  • Get Input Values: We retrieve the values from the textareas using `oldCodeInput.value` and `newCodeInput.value`.
  • Clear Previous Results: The `renderDiff` function clears the previous results by setting `element.innerHTML = ”` before rendering the new diff.
  • `performDiff` Function: This function encapsulates the diffing and rendering logic. It gets the code from the textareas, calls the `diff` function, and renders the results. It also includes error handling.
  • Event Listeners: We add `input` event listeners to the textareas. Whenever the user types in the textareas, the `performDiff` function is called, updating the diff results in real-time.

Compile and refresh your browser. Now, you can paste or type code into the textareas, and the diff results will update dynamically.

Common Mistakes and How to Fix Them

When building a code diff tool, you might encounter some common pitfalls. Here’s how to avoid or fix them:

  • Incorrect Algorithm Implementation: The core of your tool relies on the diffing algorithm. A poorly implemented algorithm can lead to inaccurate results. Double-check your logic and test it with various code snippets. Consider using a well-tested library or algorithm implementation.
  • String Handling Errors: Be mindful of how you handle strings. Incorrectly splitting strings into lines, or not handling special characters (e.g., tabs, spaces) properly, can cause issues. Use `trim()` to remove leading/trailing whitespace.
  • UI Rendering Issues: Ensure your UI updates correctly after each diff. Clear previous results before rendering new ones to avoid duplicate or incorrect displays. Use the browser’s developer tools to inspect the generated HTML and CSS.
  • Performance Problems: For very large code snippets, the diffing process can become slow. Optimize your algorithm or consider using a more efficient one. Implement techniques like lazy loading or limiting the number of lines displayed at once.
  • Error Handling: Add error handling to your code to gracefully handle unexpected situations. For example, check if the DOM elements exist before attempting to access them. Use `try…catch` blocks to handle potential exceptions.
  • Ignoring Edge Cases: Test your tool with different types of code, including empty files, files with only additions or deletions, and files with complex changes. This helps you identify and fix edge cases.

Key Takeaways

  • Understanding Code Diffing: You’ve learned the fundamental concepts behind code diffing and how it works.
  • Building a Basic Tool: You’ve built a functional web-based code diff tool using TypeScript.
  • Implementing Diff Logic: You’ve implemented a simplified diffing algorithm to compare code snippets.
  • User Interface Integration: You’ve integrated the diffing logic with a user-friendly interface using HTML, CSS, and TypeScript.
  • Enhancing the Tool: You’ve explored ways to enhance your tool with user input and other features.

FAQ

  1. What is the Myers diff algorithm?
    The Myers diff algorithm is a widely used algorithm for computing the differences between two sequences (e.g., lines of code). It’s known for its efficiency and accuracy, finding the shortest edit script (the fewest additions, deletions, and changes) to transform one sequence into another.
  2. Why is syntax highlighting important in a code diff tool?
    Syntax highlighting makes code easier to read and understand by using different colors to distinguish various code elements (keywords, variables, comments, etc.). This significantly improves the readability of the diff results, especially when dealing with complex code.
  3. How can I handle very large code files?
    For large code files, consider these optimizations: 1) Implement a more efficient diffing algorithm. 2) Use lazy loading to only render the visible portion of the diff initially. 3) Provide a mechanism to navigate through the changes (e.g., jump to the next change).
  4. What are some popular libraries for code diffing and syntax highlighting?
    Popular libraries include:

    • For diffing: `diff` (JavaScript), `jsdiff` (JavaScript), and `diff-match-patch` (various languages).
    • For syntax highlighting: Prism.js, highlight.js, and CodeMirror.
  5. Can I use this code diff tool in a production environment?
    While this tutorial provides a good starting point, the code diff tool presented here is a simplified version. For production use, you should consider using a more robust and feature-rich library or tool. Also, implement thorough testing and error handling.

The journey of building a code diff tool, like any software project, is a continuous learning experience. By iteratively improving and refining your tool, you’ll gain a deeper understanding of software development principles and enhance your skills. The ability to analyze and understand code changes is a valuable asset for any developer, and this tool provides a practical foundation for mastering this skill. Embrace the power of comparison, and let it guide you in your coding endeavors.