⚡️ comptime.ts
A dead-simple TypeScript compiler that does one thing really well: enables compile-time evaluation of expressions marked with comptime
.
This is useful for optimising your code by moving computations from runtime to compile time. This project was inspired by Bun macros and Zig comptime (hence the name).
Warning: You are responsible for ensuring that the expressions you mark with
comptime
are safe to evaluate at compile time.comptime.ts
does not perform any isolation. However, comptime imports are only allowed in project files, and not in node_modules. You may however import from node_modules as comptime.
↗️ Quick Error Reference
Contents
- What is comptime.ts?
- Examples
- Installation
- Usage
- Forcing comptime evaluation
- Running code after comptime evaluation
- How it works
- Limitations
- Best practices
- Troubleshooting
- Supporting the project
- License
What is comptime.ts?
comptime.ts allows you to evaluate expressions at compile time, similar to compile-time macros in other languages. This can help optimise your code by moving computations from runtime to compile time.
Examples
1. Simple sum function
import { sum } from "./sum.ts" with { type: "comptime" };
console.log(sum(1, 2));
Compiles to:
console.log(3);
2. Turn emotion CSS into a zero-runtime CSS library
import { css } from "@emotion/css" with { type: "comptime" };
const style = css`
color: red;
font-size: 16px;
`;
div({ class: style });
Compiles to:
const style = "css-x2wxma";
div({ class: style });
Note: The
@emotion/css
import got removed from the output. You'll need to somehow add the styles back to your project somehow. See running code after comptime evaluation for an example of emitting the styles as a CSS file. Alternatively, you might write a bundler plugin to import the CSS cache from@emotion/css
and emit them as a CSS file, etc.
3. Calculate constants at compile time
import { ms } from "ms" with { type: "comptime" };
const HOUR = ms("1 hour");
Compiles to:
const HOUR = 3600000;
Apart from function calls and tagged template literals, all sorts of expressions are supported (even complex ones like index access and simple ones like imported constants). The only limitation is that the resultant value must be serialisable to JSON.
Note: The import statements marked with
type: "comptime"
are removed in the output. We assume you have other tooling (like Vite) to handle other unused redundant statements left behind after comptime evaluation.
Installation
bun add comptime.ts
# or
pnpm add comptime.ts
# or
npm install comptime.ts
Usage
With Vite
Add the plugin to your Vite configuration:
import { comptime } from "comptime.ts/vite";
export default defineConfig({
plugins: [comptime()],
});
In case comptime.ts
is significantly slowing down your dev server, and your comptime functions behave identically at runtime and comptime, you may enable it only in production builds:
import { comptime } from "comptime.ts/vite";
export default defineConfig({
build: {
rollupOptions: {
plugins: [comptime()],
},
},
});
With Bun bundler
Add the plugin to your Bun bundler configuration:
import { comptime } from "comptime.ts/bun";
await Bun.build({
entrypoints: ["./index.ts"],
outdir: "./out",
plugins: [comptime()],
});
Command Line Interface
You can also use the CLI:
npx comptime.ts --project tsconfig.json --outdir out
Or use Bun if you prefer:
bunx --bun comptime.ts --project tsconfig.json --outdir out
Via API
Use the API directly:
import { comptimeCompiler } from "comptime.ts";
await comptimeCompiler({ tsconfigPath: "tsconfig.json" }, "./out");
Forcing comptime evaluation of arbitrary expressions (and resolving promises)
We can abuse the fact that any function imported with the type: "comptime"
option will be evaluated at compile time.
This library exports a comptime()
function that can be used to force comptime evaluation of an expression. It has to be imported with the "comptime"
attribute. Any expressions contained within it will be evaluated at compile time. If the result is a promise, the resolved value will be inlined.
Note: Technically the
comptime()
function by itself doesn't do anything by itself. It's an identity function. It's thewith { type: "comptime" }
attribute that makes the compiler evaluate the expression at compile time.
import { comptime } from "comptime.ts" with { type: "comptime" };
Use it to force comptime evaluation of an expression.
const x = comptime(1 + 2);
When the compiler is run, the expression will be evaluated at compile time.
const x = 3;
Resolving promises
const x = comptime(Promise.resolve(1 + 2));
When the compiler is run, the promise will be resolved and the result will be inlined at compile time.
const x = 3;
Note: The compiler always resolves promises returned by the evaluation, but this might not reflect in your types, in which case it's useful to use the
comptime()
function to infer the correct type.
Opting out of comptime virality
Normally, comptime.ts
will eagerly extend comptime to expressions that include a comptime expression.
import { foo } from "./foo.ts" with { type: "comptime" };
const x = foo().bar[1];
Compiles to:
const x = 2;
Notice how the whole expression, foo().bar[1]
, is evaluated at compile time. You can opt-out of this behaviour by wrapping your expression in parentheses.
const x = (foo().bar)[1];
Compiles to:
const x = ([1, 2])[1];
In this case, foo().bar
is evaluated at runtime, but [1]
is left untouched.
Note: Your formatter might remove the extraneous parentheses, so you may need to ignore the line (such as with
prettier-ignore
comments). You are of course free to extract the expression to its own line:const res = foo().bar; const x = res[1];
Compiles to:
const res = [1, 2]; const x = res[1];
This also results in only
foo().bar
being evaluated at comptime, and doesn't upset your formatter.
Running code after comptime evaluation
You can use the comptime.defer()
function to run code after comptime evaluation of all modules.
This could be used, for example, to emit collected CSS from @emotion/css
at the end of the compilation process.
import { comptime } from "comptime.ts" with { type: "comptime" };
import { css, cache } from "@emotion/css" with { type: "comptime" };
import { writeFileSync } from "node:fs" with { type: "comptime" };
const style = css`
color: red;
font-size: 16px;
`;
// ...
// You only need this once in your project, it runs after all modules are evaluated
comptime.defer(() => {
const file = Object.entries(cache.registered)
.filter(Boolean)
.map(([key, value]) => `${key} {${value}}`)
.join("\n");
writeFileSync("styles.css", file);
});
Please note that while all deferred functions are guaranteed to be executed after comptime evaluation, if multiple deferred functions exist, they are not guaranteed to be executed in any specific order because modules are evaluated concurrently.
How it Works
comptime.ts
works by:
- Parsing your TypeScript code to find imports marked with
type: "comptime"
- Finding all expressions in your files that use these imports
- Collecting an execution block by walking up the file to find all references used by the comptime expression
- Evaluating the execution block in an isolated context at compile time
- Replacing the comptime expression with the result of the execution block
Limitations
- Only JSON-serialisable values can be returned from comptime expressions
- The evaluation block is isolated, so certain runtime features might not be available
- Complex expressions might increase build time significantly
- Type information is not available during evaluation
Best Practices
- Use comptime for:
- Computing constant values
- Generating static content
- Optimising performance-critical code
- Avoid using comptime for:
- Complex runtime logic
- Side effects
- Non-deterministic operations
Troubleshooting
comptime.ts
will attempt to print very detailed error messages when it runs into an error. The message by itself should provide enough information to resolve the issue. See the error reference for more details.
If the error message is not helpful, raise an issue with the full error message and the code that's being evaluated.
However, sometimes comptime.ts
runs successfully, but the output is not what you expected. This section describes some common issues and how to resolve them.
Note: To force
comptime.ts
to print the constructed evaluation block for each expression and other debug logs, set the environment variableDEBUG=comptime:*
.
The following are some non-error issues that you might encounter:
Redundant code not removed
comptime.ts
removes imports marked withtype: "comptime"
and replaces comptime expressions.- However, it does not remove other redundant code that might be left behind after compilation.
- Use other tooling (like Vite) to handle such cleanup after the fact.
comptime.ts
is available as a standalone CLI, JavaScript API and Vite plugin. If you'd likecomptime.ts
to integrate with other tooling, please let us know via an issue or raise a PR!
Compilation result is unexpected
- Notice that variables in the caller's scope that are not comptime (imported with the "comptime" attribute) are not guaranteed to be stable.
comptime.ts
will extract their declarations, but it will not account for mutations.- If multiple comptime expressions exist in the same file, all dependent statements will be extracted and evaluated for each expression. This may cause the same declarations to be evaluated multiple times, and mutations are not reflected between evaluations.
- If you want a mutable comptime variable, declare it in another file and import it with the "comptime" attribute.
import { sum } from "./sum.ts" with { type: "comptime" }; let a = 1; const x = sum(++a, 2); ++a; const y = sum(++a, 2);
Compiles to:
let a = 1; // not a comptime var const x = 4; ++a; // untouched const y = 4; // same as previous line because it was evaluated independently
However, if we move the mutable state to another file, mutations are reflected between evaluations.
import { sum } from "./sum.ts" with { type: "comptime" }; // export const state = { a: 1 }; import { state } from "./state.ts" with { type: "comptime" }; const x = sum(++state.a, 2); ++state.a; const y = sum(state.a, 2);
Compiles to:
const x = 4; 3; // because of the ++a in previous line const y = 5;
My comptime expression was not replaced
- Check that the import has
{ type: "comptime" }
. - Ensure the expression is JSON-serialisable.
- Verify all dependencies are available at compile time.
- Check that the import has
Build time too slow
- Consider moving complex computations to runtime.
- Break down large expressions into smaller ones.
- Pass
include
/exclude
options to limit scope.
Supporting the project
A lot of time and effort goes into maintaining projects like this.
If you'd like to support the project, please consider:
- Star and share the project with others
- Sponsor the project (GitHub Sponsors / Patreon / Ko-fi)
License
MIT