1 The Problem: Multiple Document Types
When building a Quarto Typst extension, you may want to support multiple document types: reports, letters, CVs. Each type has distinct layouts, headers, and styling requirements.
While Quarto extensions can contribute multiple variants of the same base format1, this approach requires either sharing metadata across all contributed elements or duplicating it for each variant.
The dispatcher pattern offers an alternative: a single format entry point with document-type routing handled entirely in Typst code. This centralises shared logic and avoids metadata duplication.
Without a clear architecture, you end up with:
- Monolithic templates cluttered with conditional logic.
- Duplicated code across separate template files.
- Difficult maintenance as document types multiply.
The solution is a dispatcher pattern: a central function that routes to the appropriate template based on document type.
2 Quarto’s Default Typst Partials
Before building a dispatcher, it helps to understand how Quarto generates Typst documents.
Quarto provides a set of default Typst partials that handle different aspects of document generation:
| Partial | Purpose |
|---|---|
definitions.typ |
Pandoc/Quarto features (callouts, block quotes, subfloats). |
typst-template.typ |
The main template function applied to document content. |
typst-show.typ |
Show rules that capture metadata and call the template. |
page.typ |
Page properties (size, margins, numbering, background). |
notes.typ |
Footnote rendering. |
biblio.typ |
Bibliography formatting. |
The definitions.typ partial is essential and must always be included in custom templates. It provides the foundation for Quarto features like callouts.
2.1 Overriding Partials
Extensions override default partials by providing replacement files via template-partials in _extension.yml (or document YAML front matter). When you list a partial, Quarto uses your version instead of the built-in one.
The dispatcher pattern requires customising two partials:
template.typ: Load document-type templates and the dispatcher function.typst-show.typ: Route document content through the dispatcher based on configuration.
See the Quarto source for the default partial implementations.
3 The Dispatcher Pattern
The dispatcher acts as a routing hub. It receives a document type parameter, validates it, and calls the corresponding template function.
3.1 Core Implementation
document-dispatcher.typ
/// Supported document types.
1#let DOCUMENT_TYPES = ("report", "letter", "cv")
/// Main document dispatcher function.
/// Routes to the appropriate template based on document-type.
#let document-dispatcher(
2 document-type: "report",
3 ..args,
) = {
// Validate and normalise document type
4 let doc-type = if document-type in DOCUMENT_TYPES {
document-type
} else {
// Fall back to report with warning
"report"
}
// Dispatch to appropriate template
if doc-type == "letter" {
render-letter(..args)
} else if doc-type == "cv" {
render-cv(..args)
} else {
// Default: report
render-report(..args)
}
}- 1
- Define supported types as an array.
- 2
- Default document type; used when not specified.
- 3
- Variadic arguments capture all other parameters.
- 4
-
inoperator checks array membership.
3.2 Key Patterns
Pattern 1: Type Validation
The dispatcher validates the document type against a known list before routing. Invalid types fall back gracefully to a default (report) rather than failing.
let doc-type = if document-type in DOCUMENT_TYPES {
document-type
} else {
"report" // Safe fallback
}Pattern 2: Variadic Argument Forwarding
Each document type may accept different parameters. Rather than listing every possible parameter, the dispatcher captures them all with ..args and forwards them to the target function.
#let document-dispatcher(document-type: "report", ..args) = {
// ...
render-report(..args) // Forward all arguments
}This means:
- The dispatcher does not need to know about each template’s specific parameters.
- Adding new parameters to a template requires no changes to the dispatcher.
- Each template function handles only its own parameters.
Pattern 3: Conditional Routing
Typst’s if-else statements handle the routing logic. For more document types, this could be extended with a dictionary lookup:
#let TEMPLATE_FUNCTIONS = (
"report": render-report,
"letter": render-letter,
"cv": render-cv,
)
// Then dispatch with:
let template-fn = TEMPLATE_FUNCTIONS.at(doc-type, default: render-report)
template-fn(..args)4 Configuration via YAML
Users specify the document type in their Quarto document’s YAML front matter.
4.1 User-Facing Configuration
document.qmd
---
title: "My Report"
format:
my-extension-typst:
document-type: report
---The extension’s format name (my-extension-typst) comes from _extension.yml. The document-type key is passed through Quarto’s template processing.
4.2 Bridging Quarto and Typst
The typst-show.typ partial connects YAML metadata to the Typst dispatcher:
typst-show.typ
#show: document-dispatcher.with(
// Document type selection
1 document-type: $if(document-type)$"$document-type$"$else$"report"$endif$,
// Pass through other parameters
title: [$title$],
author: [$for(author)$$author$$sep$, $endfor$],
date: [$date$],
// ... additional parameters
)- 1
-
Pandoc template syntax (
$if()$) checks if the metadata key exists and provides a default.
The $if(document-type)$...$else$...$endif$ pattern:
- Uses the user-provided value if present.
- Falls back to “report” if not specified.
- Ensures the dispatcher always receives a valid string.
5 File Organisation
A well-organised extension separates document types into distinct files.
5.1 Directory Structure
_extensions/my-extension/
├── _extension.yml
├── template.typ # Main entry point
├── typst-show.typ # Quarto-Typst bridge
└── partials/
├── document-dispatcher.typ
└── document-types/
├── report.typ
├── letter.typ
└── cv.typ5.2 Extension Manifest
The _extension.yml declares template partials in loading order:
_extension.yml
title: My Extension
version: 1.0.0
contributes:
formats:
my-extension-typst:
template: template.typ
template-partials:
- partials/document-types/report.typ # Load document types first
- partials/document-types/letter.typ
- partials/document-types/cv.typ
- partials/document-dispatcher.typ # Dispatcher last (uses the above)5.3 Main Template
The template.typ file serves as the entry point that assembles all components. It must include Quarto’s essential definitions and load your custom partials in the correct order.
template.typ
// Essential: Quarto/Pandoc feature definitions (callouts, quotes, etc.)
$definitions.typ()$
// Load document type templates
$report.typ()$
$letter.typ()$
$cv.typ()$
// Load dispatcher (must come after document types)
$document-dispatcher.typ()$The loading order matters:
definitions.typmust come first; it provides Quarto features like callouts.- Document type templates define the rendering functions (
render-report, etc.). - Dispatcher comes last because it references the document type functions.
The $partial-name.typ()$ syntax is Quarto’s template interpolation. It inserts the contents of the corresponding partial file at that location.
6 Adding New Document Types
To add a new document type (e.g., “memo”):
Step 1: Create the template function.
partials/document-types/memo.typ
#let render-memo(
title: none,
to: none,
from: none,
date: none,
body,
) = {
// Memo-specific layout
set page(paper: "a4", margin: 2cm)
// Header
grid(
columns: (1fr, 1fr),
[*TO:* #to], [*DATE:* #date],
[*FROM:* #from], [],
)
line(length: 100%)
// Subject
if title != none {
align(center, text(weight: "bold", size: 14pt, title))
}
// Body
body
}Step 2: Register in the dispatcher.
#let DOCUMENT_TYPES = ("report", "letter", "cv", "memo")
// Add routing case
if doc-type == "memo" {
render-memo(..args)
}Step 3: Add to extension manifest.
template-partials:
- partials/document-types/memo.typ
# ... other partialsStep 4: Load in template.
$memo.typ()$7 Benefits of This Pattern
Clean Separation
Each document type lives in its own file with its own logic. Changes to one type do not affect others.
Extensibility
Adding new document types requires minimal changes to existing code. The dispatcher’s validation list and routing logic are the only modifications needed.
Graceful Fallbacks
Invalid document types fall back to a sensible default rather than failing. Users receive working output even with typos in their configuration.
Parameter Isolation
Each template function defines only its own parameters. The dispatcher does not need to understand every possible parameter combination.
8 Conclusion
The document-type dispatcher pattern provides a clean architecture for Quarto Typst extensions that support multiple document types.
Key takeaways:
- Central routing: A single dispatcher function routes to appropriate templates.
- Validation with fallbacks: Check document types against a known list; default gracefully.
- Variadic forwarding: Use
..argsto pass parameters without coupling. - YAML integration: Bridge Quarto metadata to Typst via
typst-show.typ. - Modular organisation: Separate files for each document type.
The dispatcher pattern scales well as your extension grows. Start with one or two document types, and the architecture supports adding more without refactoring.
8.1 Further Resources
- Typst Documentation: Functions.
- Quarto Documentation: Typst Partials.
- quarto-mcanouil Extension - demonstrates this pattern in production.
Footnotes
Format variants use the
+syntax in_extension.yml:_extension.yml
contributes: formats: typst+report: title: "Typst Report" typst+article: title: "Typst Article"Then in a document:
↩︎YAML
format: myformat-typst+report: default
Reuse
Citation
@misc{canouil2026,
author = {CANOUIL, Mickaël},
title = {Document-Type {Dispatching} in {Quarto} {Typst} {Extensions}},
date = {2026-01-19},
url = {https://mickael.canouil.fr/posts/2026-01-19-typst-document-dispatcher/},
langid = {en-GB}
}
