Why can't I use a function in a chapter even though I imported it at the start of my main file?

My main Typst file looks like this:

#import "funcs.typ": my-func

#include "chapter.typ"

However, when I call my-func() inside the chapter file, I get the error “unknown variable: my-func”. Calling it in the main file, however, works. How can I fix that?

You need to define (in this case, import) my-func() in the file you want to use it, i.e. chapter.typ. Imports don’t “carry over” from the files including them.

If you have many imports that you don’t want to repeat in many files, you can bundle them in a single file, like this:

// prelude.typ
#import "@preview/package:0.1.0"
#import "funcs.typ": my-func

 

// main.typ
#import "prelude.typ": *

#package.some-function()

#include "chapter.typ"

 

// chapter.typ
#import "prelude.typ": *

#my-func()

The name prelude is just a suggestion; it comes from programming languages that use that name for their set of “default” imports that are automatically available.


Explanation: If you’re used to #include from C, it may come as a surprise that Typst’s import and include features work fairly differently. With C-style includes, the code from chapter.typ would be effectively “copy/pasted” into your main file and, because my-func() is available in the main file, it would also work in the included chapter.

Typst’s name resolution instead uses so-called lexical scoping: variables (including functions) are only available if they’re defined in the surrounding code. Take this example:

#let greeting = "Hello"

#let greet() = {
  greeting + " World"
}

#greet()

Here, the variable greeting can be used in greet() because the functions’s body is surrounded by the rest of the file, where the variable is defined. Calling greet() works because the code at the same “level” and before the code also counts.

The file itself is not “surrounded” by anything*, so a function such as my-func() would not be available; you can’t do anything from outside to change that.

*The exception is that built-in modules and names such as math or heading are always available; they can be thought of as being “defined in surrounding code” and form the proper prelude that Typst code has access to.

2 Likes

I have actually desperately been trying to find an “inline import” method in Typst. It would solve so many of my little issues and make the templates I make easier to use for others.

For example I use acrostiche for acronym management. it would be great if people could just use acrostiche in their files without having to specifically import it.

I’m not saying the behaviour of the current #import should be changed. I’m saying another function. “#input” or “#inline-include” or something.

2 Likes

Nice if that could be configured with a settings/manifest file in the same directory, a little like a cargo.toml file.

This is actually possible to do:

#let my-import(pkg) = {
  import pkg as p
  p
}

#my-import("@preview/example:0.1.0").add(1, 2)

One problem is that this only works for package imports; for files you’d run into path problems, such as here: Why are paths always relative to the current file? The other problem is of course that, unless this was built-in, you’d still need to import or write that function yourself…


Personally I think the way to allow this is to allow specifying an actual prelude somehow. That “somehow” would probably be the typst.toml file that already contains metadata for packages, and make it more appropriate for documents in addition to packages (and templates).

Using a prelude file for this would mean that the regular Typst import and let statements could still be used, they’d very naturally specify the package versions to use at a single location, and not require completely new concepts.


The downside in general of any such approach is that Typst code becomes less “copy-pasteable”: right now, you can look at the file and find everything that the code you’re copying needs; either directly there or in it imports. (Well there’s already one tiny caveat to that: your file may require a specific version of Typst to compile, which is not written down in every file.)

1 Like

I think portability is a great default. But IMO it should be just that: a default that can be overridden, subject to a few compiler warnings or what-have-you. If we really insist on plopping a package or template file in some arbitrary directory as a repository for useful functions, on our own heads be it. It would still be possible to write a “scoop it all up and package it” script, a little like the way Indesign can collect and package all assets, properly linked, ready for despatch to printers or publishers…

If we really insist on plopping a package or template file in some arbitrary directory as a repository for useful functions, on our own heads be it.

As a brief comment, I wanted to mention that this is already possible. Consider the project structure below:

project
├── some
│   └── sub
│       └── folder
│           └── file.typ
└── template.typ

You can import your template from anywhere in your project easily through /absolute/paths/from/project/root, by just adding the line at the top of the file.typ example below:

// template.typ

#let func(x) = [Hello #x]

// file.typ

// Note the '/' before template.typ
// Works at any level of nesting
#import "/template.typ": *

// Can use
#func[ABC ABC ABC]

I’d consider this equivalent to a per-project preamble (even more flexible than that, actually, since you can pick what to import, import multiple files or a different file, and so on).
You can even use symlinks here if you want template.typ to point at some arbitrary file in your system which could change depending on a script you write (for example).

So the desired functionality is retained, just in a way that is more integrated with the Typst language, and also more readable and portable as was already mentioned.

3 Likes