If at first you don't succeed,

Trie, Trie again.

Meet the Trie (a.k.a Prefix Tree)

class TrieNode {
  constructor() {
    this.children = {};   // maps characters -> TrieNode
    this.isEndOfWord = false;
  }
}

class Trie {
  constructor() {
    this.root = new TrieNode();
  }
}

The Basics

  insert(word) {
    let node = this.root;

    for (let char of word) {
      if (!node.children[char]) {
        node.children[char] = new TrieNode();
      }
      node = node.children[char];
    }

    node.isEndOfWord = true;
  }

Getting things in

 search(word) {
    let node = this.root;

    for (let char of word) {
      if (!node.children[char]) {
        return false;
      }
      node = node.children[char];
    }

    return node.isEndOfWord;
  }

Where is it?

startsWith(prefix) {
  let node = this.root;

  for (let char of prefix) {
    if (!node.children[char]) {
      return false;
    }
    node = node.children[char];
  }

  return true;
}

Maybe just a bit

Cool story bro, but like, what now?

A real-er example

Auto Complete

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <style>
    body {
      font-family: sans-serif;
      padding: 40px;
      max-width: 700px;
      margin: auto;
    }

    input {
      width: 100%;
      padding: 12px;
      font-size: 18px;
      border: 2px solid #ccc;
      border-radius: 8px;
    }

    ul {
      list-style: none;
      padding: 0;
      margin-top: 10px;
      border: 1px solid #ddd;
      border-radius: 8px;
      overflow: hidden;
    }

    li {
      padding: 10px;
      border-bottom: 1px solid #eee;
      cursor: pointer;
    }

    li:last-child {
      border-bottom: none;
    }

    li:hover,
    li.active {
      background: #f0f0f0;
    }

    .match {
      font-weight: bold;
      color: darkblue;
    }

    .hint {
      margin-top: 8px;
      color: #666;
      font-size: 14px;
    }
  </style>
</head>

<body>
  <h1>Trie-based Autocomplete</h1>
  <p>Start typing a JavaScript-related term:</p>

  <input id="search" placeholder="Try: rea, dom, async..." />
  <p class="hint">
    ↑ ↓ to navigate • Enter to select • Esc to close • Top 10 matches
  </p>

  <ul id="suggestions"></ul>

  <script>
    // ----------------------------
    // Trie Implementation
    // ----------------------------

    class TrieNode {
      constructor() {
        this.children = {};
        this.isEndOfWord = false;
      }
    }

    class Trie {
      constructor() {
        this.root = new TrieNode();
      }

      insert(word) {
        let node = this.root;

        for (let char of word) {
          if (!node.children[char]) {
            node.children[char] = new TrieNode();
          }
          node = node.children[char];
        }

        node.isEndOfWord = true;
      }

      _collectWords(node, prefix, results, limit) {
        if (results.length >= limit) return;

        if (node.isEndOfWord) {
          results.push(prefix);
        }

        for (let char in node.children) {
          if (results.length >= limit) return;
          this._collectWords(node.children[char], prefix + char, results, limit);
        }
      }

      autocomplete(prefix, limit = 10) {
        let node = this.root;

        for (let char of prefix) {
          if (!node.children[char]) return [];
          node = node.children[char];
        }

        let results = [];
        this._collectWords(node, prefix, results, limit);
        return results;
      }
    }

    // ----------------------------
    // Example Word List (~200)
    // ----------------------------

    const words = [
      "array", "arrowfunction", "async", "await", "api",
      "babel", "bind", "boolean", "browser",
      "callback", "canvas", "catch", "chrome", "class",
      "closure", "component", "const", "constructor",
      "context", "cors", "cssom",

      "data", "debugger", "declaration", "default",
      "deno", "destructuring", "dom", "document",

      "ecmascript", "event", "eventloop", "express", "export",

      "fetch", "finally", "firebase", "function",

      "garbagecollection", "generator", "github", "global", "graphql",

      "hoisting", "hook", "html", "http",

      "immutable", "import", "inheritance", "instanceof",

      "javascript", "jest", "json", "jsx",

      "koa",

      "let", "library", "lint", "lodash",

      "map", "middleware", "minification", "module",
      "mongodb", "mocha",

      "node", "nodejs", "npm",

      "object", "oop", "operator",

      "package", "parse", "polyfill", "promise",
      "prototype", "proxy",

      "queryselector",

      "react", "reactdom", "reactrouter", "reducer",
      "referenceerror", "regexp", "render",

      "scope", "script", "server", "settimeout",
      "spread", "state", "string", "svelte", "symbol",

      "tailwind", "template", "test", "this", "throw",
      "typescript",

      "undefined", "url",

      "variable", "vite", "vue",

      "webpack", "websocket", "webworker", "webassembly",

      "xhr", "yarn", "yield", "zod"
    ];

    // ----------------------------
    // Build Trie
    // ----------------------------

    const trie = new Trie();
    words.forEach(word => trie.insert(word));

    // ----------------------------
    // UI Logic (Highlight + Keyboard)
    // ----------------------------

    const input = document.getElementById("search");
    const suggestionsList = document.getElementById("suggestions");

    let activeIndex = -1;
    let currentSuggestions = [];

    // Highlight matching prefix
    function highlightMatch(word, prefix) {
      const start = word.slice(0, prefix.length);
      const rest = word.slice(prefix.length);

      return `<span class="match">${start}</span>${rest}`;
    }

    // Render suggestion list
    function renderSuggestions(prefix) {
      suggestionsList.innerHTML = "";
      activeIndex = -1;

      if (!prefix) {
        currentSuggestions = [];
        return;
      }

      currentSuggestions = trie.autocomplete(prefix, 10);

      currentSuggestions.forEach((word, index) => {
        const li = document.createElement("li");
        li.innerHTML = highlightMatch(word, prefix);

        li.addEventListener("click", () => {
          input.value = word;
          suggestionsList.innerHTML = "";
        });

        suggestionsList.appendChild(li);
      });
    }

    // Update active highlight
    function updateActiveItem() {
      const items = suggestionsList.querySelectorAll("li");

      items.forEach(item => item.classList.remove("active"));

      if (activeIndex >= 0 && items[activeIndex]) {
        items[activeIndex].classList.add("active");
      }
    }

    // Input typing handler
    input.addEventListener("input", () => {
      const value = input.value.trim().toLowerCase();
      renderSuggestions(value);
    });

    // Keyboard navigation handler
    input.addEventListener("keydown", (e) => {
      if (currentSuggestions.length === 0) return;

      if (e.key === "ArrowDown") {
        activeIndex++;
        if (activeIndex >= currentSuggestions.length) activeIndex = 0;
        updateActiveItem();
      }

      if (e.key === "ArrowUp") {
        activeIndex--;
        if (activeIndex < 0) activeIndex = currentSuggestions.length - 1;
        updateActiveItem();
      }

      if (e.key === "Enter") {
        if (activeIndex >= 0) {
          input.value = currentSuggestions[activeIndex];
          suggestionsList.innerHTML = "";
          currentSuggestions = [];
        }
      }

      if (e.key === "Escape") {
        suggestionsList.innerHTML = "";
        currentSuggestions = [];
      }
    });
  </script>
</body>
</html>

But how?

_collectWords(node, prefix, results, limit) {
  if (results.length >= limit) return;

  if (node.isEndOfWord) {
    results.push(prefix);
  }

  for (let char in node.children) {
    if (results.length >= limit) return;
    this._collectWords(node.children[char], prefix + char, results, limit);
  }
}



autocomplete(prefix, limit = 10) {
  let node = this.root;

  for (let char of prefix) {
    if (!node.children[char]) return [];
    node = node.children[char];
  }

  let results = [];
  this._collectWords(node, prefix, results, limit);
  return results;
}

Fuzzy Matching

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <style>
    body {
      font-family: sans-serif;
      padding: 40px;
      max-width: 750px;
      margin: auto;
    }

    input {
      width: 100%;
      padding: 12px;
      font-size: 18px;
      border: 2px solid #ccc;
      border-radius: 10px;
      outline: none;
    }

    input:focus {
      border-color: #444;
    }

    ul {
      list-style: none;
      padding: 0;
      margin-top: 12px;
      border: 1px solid #ddd;
      border-radius: 10px;
      overflow: hidden;
    }

    li {
      padding: 10px;
      border-bottom: 1px solid #eee;
      cursor: pointer;
      font-size: 16px;
    }

    li:last-child {
      border-bottom: none;
    }

    li:hover,
    li.active {
      background: #f0f0f0;
    }

    .match {
      font-weight: bold;
      color: darkblue;
    }

    .hint {
      margin-top: 10px;
      color: #666;
      font-size: 14px;
    }
  </style>
</head>

<body>
  <h1>Trie Autocomplete + Fuzzy Search</h1>

  <p>Start typing a JS related word (typos allowed):</p>

  <input
    id="search"
    placeholder="Try: reac, javsacript, promse..."
    autocomplete="off"
  />

  <p class="hint">
    ↑ ↓ navigate • Enter select • Esc close • Top 10 • Max typo distance = 1
  </p>

  <ul id="suggestions"></ul>

  <script>
    // =====================================================
    // Trie Implementation (with Fuzzy Search)
    // =====================================================

    class TrieNode {
      constructor() {
        this.children = {};
        this.isEndOfWord = false;
      }
    }

    class Trie {
      constructor() {
        this.root = new TrieNode();
      }

      insert(word) {
        let node = this.root;

        for (let char of word) {
          if (!node.children[char]) {
            node.children[char] = new TrieNode();
          }
          node = node.children[char];
        }

        node.isEndOfWord = true;
      }

      // -----------------------------------------------------
      // FUZZY AUTOCOMPLETE (Levenshtein-style Trie DFS)
      // maxEdits = number of typos allowed
      // -----------------------------------------------------

      fuzzyAutocomplete(input, maxEdits = 1, limit = 10) {
        let results = new Set();

        const dfs = (node, wordSoFar, index, editsUsed) => {
          if (results.size >= limit) return;
          if (editsUsed > maxEdits) return;

          // If we've consumed all input characters
          if (index === input.length) {
            if (node.isEndOfWord) {
              results.add(wordSoFar);
            }

            // Allow extra letters beyond input (insertion)
            for (let char in node.children) {
              dfs(node.children[char], wordSoFar + char, index, editsUsed + 1);
            }
            return;
          }

          const currentChar = input[index];

          // Case 1: Exact match
          if (node.children[currentChar]) {
            dfs(
              node.children[currentChar],
              wordSoFar + currentChar,
              index + 1,
              editsUsed
            );
          }

          // Case 2: Substitution (wrong character)
          for (let char in node.children) {
            if (char !== currentChar) {
              dfs(
                node.children[char],
                wordSoFar + char,
                index + 1,
                editsUsed + 1
              );
            }
          }

          // Case 3: Deletion (skip input character)
          dfs(node, wordSoFar, index + 1, editsUsed + 1);

          // Case 4: Insertion (extra character added)
          for (let char in node.children) {
            dfs(
              node.children[char],
              wordSoFar + char,
              index,
              editsUsed + 1
            );
          }
        };

        dfs(this.root, "", 0, 0);

        return [...results].slice(0, limit);
      }
    }

    // =====================================================
    // JavaScript-Themed Word List (~200)
    // =====================================================

    const words = [
      "array", "async", "await", "api", "argument",
      "babel", "backend", "bind", "boolean", "browser",
      "buffer", "bundler",

      "callback", "canvas", "catch", "chai", "chrome",
      "class", "closure", "cli", "component", "const",
      "constructor", "context", "cors", "cookie",

      "data", "datatype", "debugger", "declaration",
      "default", "deno", "destructuring", "dom",
      "document", "docker",

      "ecmascript", "event", "eventloop", "exception",
      "express", "export", "eslint",

      "fetch", "finally", "firebase", "framework",
      "frontend", "fullstack", "function",

      "garbagecollection", "generator", "getelementbyid",
      "github", "global", "graphql",

      "hashmap", "heroku", "hoisting", "hook",
      "html", "http", "https",

      "immutable", "import", "indexdb", "inheritance",
      "instanceof", "iteration",

      "javascript", "jest", "json", "jsx", "jwt",

      "koa", "kubernetes",

      "lambda", "let", "library", "lint", "lodash",
      "localstorage", "loop",

      "map", "memoization", "middleware", "minification",
      "module", "mongodb", "mocha", "mutationobserver",

      "nan", "nestjs", "netlify", "nextjs",
      "node", "nodejs", "npm", "nuxt",

      "object", "observer", "oop", "operator",
      "openapi", "oauth",

      "package", "parse", "performance", "pipe",
      "polyfill", "promise", "prototype", "proxy",
      "prisma", "prettier",

      "queryselector", "queue", "queuemicrotask",

      "react", "reactdom", "reactrouter", "reducer",
      "referenceerror", "regexp", "render", "rest",
      "runtime", "rxjs",

      "scope", "script", "server", "serverless",
      "sessionstorage", "settimeout", "singleton",
      "solidjs", "spread", "spa", "sql", "stack",
      "state", "static", "string", "svelte", "symbol",

      "tailwind", "template", "test", "this",
      "throw", "token", "tree", "trycatch",
      "typescript",

      "undefined", "unittest", "unpkg", "url",
      "urlparams",

      "value", "variable", "vdom", "vercel",
      "virtualmachine", "vite", "vue",

      "webpack", "websocket", "webworker",
      "webassembly", "webrtc", "window",

      "xhr", "xmlhttprequest",

      "yarn", "yield",

      "zod"
    ];

    // =====================================================
    // Build Trie
    // =====================================================

    const trie = new Trie();
    words.forEach(word => trie.insert(word));

    // =====================================================
    // UI + Highlight + Keyboard Navigation
    // =====================================================

    const input = document.getElementById("search");
    const suggestionsList = document.getElementById("suggestions");

    let activeIndex = -1;
    let currentSuggestions = [];

    // Highlight matching letters (best effort)
    function highlightMatch(word, typed) {
      let result = "";
      let i = 0;

      for (let char of word) {
        if (i < typed.length && char === typed[i]) {
          result += `<span class="match">${char}</span>`;
          i++;
        } else {
          result += char;
        }
      }

      return result;
    }

    // Render suggestions
    function renderSuggestions(value) {
      suggestionsList.innerHTML = "";
      activeIndex = -1;

      if (!value) {
        currentSuggestions = [];
        return;
      }

      // FUZZY search here
      currentSuggestions = trie.fuzzyAutocomplete(value, 1, 10);

      currentSuggestions.forEach((word, index) => {
        const li = document.createElement("li");
        li.innerHTML = highlightMatch(word, value);

        li.addEventListener("click", () => {
          input.value = word;
          suggestionsList.innerHTML = "";
        });

        suggestionsList.appendChild(li);
      });
    }

    // Update active selection style
    function updateActiveItem() {
      const items = suggestionsList.querySelectorAll("li");

      items.forEach(item => item.classList.remove("active"));

      if (activeIndex >= 0 && items[activeIndex]) {
        items[activeIndex].classList.add("active");
      }
    }

    // Input typing
    input.addEventListener("input", () => {
      const value = input.value.trim().toLowerCase();
      renderSuggestions(value);
    });

    // Keyboard navigation
    input.addEventListener("keydown", (e) => {
      if (currentSuggestions.length === 0) return;

      if (e.key === "ArrowDown") {
        activeIndex++;
        if (activeIndex >= currentSuggestions.length) activeIndex = 0;
        updateActiveItem();
      }

      if (e.key === "ArrowUp") {
        activeIndex--;
        if (activeIndex < 0) activeIndex = currentSuggestions.length - 1;
        updateActiveItem();
      }

      if (e.key === "Enter") {
        if (activeIndex >= 0) {
          input.value = currentSuggestions[activeIndex];
          suggestionsList.innerHTML = "";
          currentSuggestions = [];
        }
      }

      if (e.key === "Escape") {
        suggestionsList.innerHTML = "";
        currentSuggestions = [];
      }
    });
  </script>
</body>
</html>

Likee Dis

  fuzzyAutocomplete(input, maxEdits = 1, limit = 10) {
    let results = new Set();

    const dfs = (node, wordSoFar, index, editsUsed) => {
      if (results.size >= limit) return;
      if (editsUsed > maxEdits) return;

      if (index === input.length) {
        if (node.isEndOfWord) {
          results.add(wordSoFar);
        }
        for (let char in node.children) {
          dfs(node.children[char], wordSoFar + char, index, editsUsed + 1);
        }
        return;
      }
      const currentChar = input[index];

      // Case 1: Exact match
      if (node.children[currentChar]) {
        dfs(
          node.children[currentChar],
          wordSoFar + currentChar,
          index + 1,
          editsUsed
        );
      }

      // Case 2: Substitution (wrong character)
      for (let char in node.children) {
        if (char !== currentChar) {
          dfs(
            node.children[char],
            wordSoFar + char,
            index + 1,
            editsUsed + 1
          );
        }
      }

      // Case 3: Deletion (skip input character)
      dfs(node, wordSoFar, index + 1, editsUsed + 1);

      // Case 4: Insertion (extra character added)
      for (let char in node.children) {
        dfs(
          node.children[char],
          wordSoFar + char,
          index,
          editsUsed + 1
        );
      }
    };

    dfs(this.root, "", 0, 0);
    return [...results].slice(0, limit);
  }
}

Rezults

Want to know moar?

✨ Levenshtein-style Trie DFS ✨

Want to know moar?

Go forth, and Trie your best!

Data Structure - Trie

By signupskm

Data Structure - Trie

  • 47