Quartz Type Safety

Patterns and anti-patterns for TypeScript type safety in Quartz inline scripts.

Type Definitions

Global Type Augmentation

Quartz inline scripts run in a browser context with custom globals (e.g., window.addCleanup, custom events). TypeScript needs declaration files to understand these.

Solution: Create a centralized types/quartz.d.ts with proper declarations.

// types/quartz.d.ts
declare global {
  interface Window {
    addCleanup(fn: () => void): void;
    spaNavigate(url: URL | FullSlug): void;
  }
}

See global-type-augmentation for the complete pattern.

Common Errors

1. Missing Type Definitions

Problem: TypeScript errors for SCSS modules, Window extensions, and custom events.

Symptoms:

TS2339: Property 'addCleanup' does not exist on type 'Window'
TS2307: Cannot find module '*.scss' or its corresponding type declarations

Solution: Create types/quartz.d.ts with:

  • SCSS module declarations
  • Window interface extensions
  • Custom event type mappings
  • Content data type definitions

2. Async Function Invocation

Problem: Calling a factory function without parentheses.

// WRONG - fetchData is a function, not a promise
const data = await fetchData;
 
// CORRECT - call the function
const data = await fetchData();

Root Cause: fetchData is a factory function that returns a Promise. Without parentheses, you’re awaiting the function reference itself (which resolves immediately to the function).

Files Affected: graph.inline.ts, search.inline.ts

3. Library API Misuse

Problem: Using outdated or incorrect library method signatures.

// WRONG - old FlexSearch signature
index.addAsync(id++, { id, slug, title, content, tags });
 
// CORRECT - FlexSearch expects document as single parameter
index.addAsync({ id, slug, title, content, tags });

Lesson: Always verify library APIs after version updates. TypeScript strict mode catches these at compile time.

4. Optional Field Handling

Problem: Accessing optional fields without null checks.

// WRONG - may be undefined
const tags = contentDetails.tags.join(', ');
 
// CORRECT - use null coalescing
const tags = (contentDetails.tags ?? []).join(', ');

Context: ContentDetails fields (title, tags, content) are optional. Always provide defaults.

Anti-Patterns

Timing-Based Initialization

// WRONG - unreliable race condition workaround
requestAnimationFrame(() => init());
setTimeout(() => init(), 50);
setTimeout(() => init(), 200);
 
// CORRECT - use SPA navigation events
document.addEventListener('nav', () => init());

Missing Error Boundaries

// WRONG - unhandled promise rejection
document.addEventListener('nav', async () => {
  await initializeGraph();
});
 
// CORRECT - wrap in try-catch with component prefix
document.addEventListener('nav', async () => {
  try {
    await initializeGraph();
  } catch (error) {
    console.error('[Graph] Init failed:', error);
  }
});

Inline Type Assertions

// WRONG - bypasses type checking
const data = (await fetchData()) as any;
 
// CORRECT - proper type declaration
const data: ContentIndex = await fetchData();

Best Practices

  1. Centralized type definitions - Single types/ directory for all declarations
  2. Error boundary with component prefixes - [Graph] Init failed: for easy debugging
  3. Strict TypeScript checking - Catches runtime errors at compile time
  4. Build verification before deploy - CI workflow gates prevent broken deployments
  5. Null coalescing for optional fields - Default values prevent runtime errors