Library / SDK
paholg/typenum avatar
paholg/typenum

typenum: arithmetic on numbers that exist only in the type system

Compile time numbers in Rust.

590 stars61 forksRustNOASSERTION

At a glance

What is it?
Typenum is a Rust library for type-level numbers evaluated at compile time, covering bits, unsigned integers and signed integers. It is the kind of crate you either need exactly or not at all, and its value shows up in the crates that depend on it for compile-time checked dimensions and fixed-size arrays.
Who is it for?
Adopt typenum if you are writing a Rust crate where a length, a bit width or a unit quantity has to be part of the type rather than a runtime value, since it gives you arithmetic on those numbers with no runtime cost and no dependency weight. Do not adopt it if a const generic or a plain associated constant would do, because the compile times and the error messages are the price of the type-level route.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 147 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Numbers that exist only as types

Typenum is a Rust library for type-level numbers evaluated at compile time. It currently supports bits, unsigned integers and signed integers.

The idea is that a number can be carried in the type system rather than in a value. A type like P3 represents the number three, and because it is a type it can be passed to a function, returned from one, and constrained by a bound. You cannot accidentally add a runtime length to a type-level length, because the compiler refuses to mix them.

The README gives a trivial example that shows the shape:

rust
use typenum::{Sum, Exp, Integer, N2, P3, P4};

type X = Sum<P3, P4>;
assert_eq!(<X as Integer>::to_i32(), 7);

type Y = Exp<N2, P3>;
assert_eq!(<Y as Integer>::to_i32(), -8);

Sum adds two type-level numbers, so Sum of P3 and P4 is seven, and the assertion converts it to a runtime i32 to check it. Exp raises one to the power of the other, and the sign follows the convention that a negative base with an odd exponent gives a negative result, which is why N2 to the power of P3 is negative eight.

The interesting part is that the assertion is not the point. The point is that the type X exists and can be used as a length, a width or a parameter. The runtime conversion is there to let you check your work in a test.

The type names encode the sign. P is positive, N is negative, and the digit after it is the magnitude, so P3 is positive three and N2 is negative two. The type-level numbers are ordinary types, and the arithmetic on them is implemented as trait impls that the compiler resolves.

One import path, whatever the module layout

The library is divided into several modules, but they are all re-exported through the crate root, so anything in the crate can be imported with a single use typenum::whatever; and the crate structure can be ignored.

There is a second convenience, which is the one most Rust code reaches for first. You can treat the consts module as a prelude and perform a glob import. The consts module is where the library keeps the primitive type-level numbers, so a glob import brings in the whole set of small integers you would otherwise be naming one at a time.

That import arrangement is a deliberate ergonomic decision, and it has a cost worth naming. Flattening the module tree means the names you get are the crate's names, and if your own code has a type called U4 or P2 there is a collision to resolve. In exchange, a type-level number is a single path segment rather than a nested one, which matters when a signature is full of them.

The other structural point is what the crate does not re-export. There is no runtime numeric type here to collide with the standard library's integers. Everything typenum offers is a type, and the only way to get a value out of one is the Integer trait's conversion, which is what the assertions in the example use.

Feature flags decide how much arithmetic you get

The package manifest lists five features, and each one changes what the library can do rather than what it can be used with.

The const-generics feature is the significant one in modern Rust. Without it, the type-level numbers are the hand-written or generated types the crate provides, and arithmetic on them is resolved through trait impls. With it, the crate can work with the compiler's own const generic machinery, which is faster to compile and integrates with the rest of the ecosystem's const generic code.

The i128 feature extends the supported integer range to 128 bits. The strict feature is a set of extra checks, and the justfile runs the whole test suite with it enabled, both the tests and the documentation build, so strict is what the project itself treats as the configuration to keep working.

The scale_info feature is the odd one out, because it pulls in an external dependency, scale-info at version 1.0 with default features off and optional. The feature is named scale_info and it enables the derive on that dependency, which is how a type-level number can be described in the metadata that the Rust compiler emits for the type information tooling, the kind of output that cargo metadata and the various type introspection tools consume.

Note the category in the manifest: no-std. The library is usable in environments without the standard library, which is the main practical constraint on where it can be used.

libcore only, so it works where std does not

Typenum depends only on libcore, and so it is suitable for use on any platform.

That is the design decision that makes the crate usable in the places where type-level numbers actually matter. A crate that runs on an embedded target with no allocator, no file system and no threads still has a compiler that can evaluate type-level arithmetic, and libcore is what the compiler provides to every target. Depending on std would rule out exactly the environments where a fixed-size buffer type is most useful.

The category in the manifest, no-std, is the packaging expression of that. The minimum supported Rust version is declared as 1.41.0 and the edition is 2018, which is a conservative floor rather than a recent one, and that is consistent with a library that has been around a long time and has to keep compiling on old toolchains.

The library also provides a type-level array of type-level numbers, and the manifest is honest that its implementation is incomplete. That is a sentence worth reading before you design a type around it, because the array is the part of the surface most likely to have a hole in it, and the fact that the crate's own description says so is more useful than a version number would have been.

The justfile is how the project builds itself

The repository uses a justfile rather than a Makefile, and the targets describe the whole development loop of a crate whose source is partly generated.

The gen target produces the generated code and formats it:

code
    cargo run --package generate
    cargo fmt

The generate directory is a workspace member, and the manifest declares the workspace with that single member. So the crate's own source contains generated types, and regeneration is a normal step rather than an occasional chore.

The lint target fans out into three checks, a formatting check, clippy with the standard feature set, and clippy with all features:

code
    cargo fmt --all -- --check
code
    cargo clippy -- -D warnings
code
    cargo clippy --all-features -- -D warnings

Warnings are errors in both clippy invocations, and the top level test-local target chains gen, lint and test together, then runs the tests with the const-generics feature enabled on top of that. The test target itself runs the test suite and the documentation build with the strict feature:

code
    cargo test --verbose --features "strict" {{args}}
    cargo doc --features "strict" {{args}}

Building the documentation under the strict feature is a deliberate choice, since it means the examples in the docs are checked against the same configuration as the tests. The up target is the one that refreshes dependencies, through nix flake update and cargo update.

The repository also carries a clippy.toml, a flake.nix with a flake.lock, an .envrc for direnv, and a Cargo.lock. The Nix flake is how the toolchain is pinned for contributors who use it, and the justfile is the entry point for everything else.

Error messages are the known weak point, and tnfilt is the fix

The README is unusually candid about this. Typenum's error messages aren't great, and can be difficult to parse.

That is a predictable consequence of the design rather than a separate shortcoming. When a type-level expression fails to resolve, the compiler has to explain a failure in trait resolution, and the resulting message is a wall of type names, associated constants and candidate impls rather than a sentence. For a library whose whole purpose is to be read by the compiler, having the compiler's output be unreadable is a real cost.

The project points at a tool for this. The people at Auxon have written one, tnfilt, and the README asks you to take a look at it. The idea is to filter the compiler's diagnostics down to the part that matters for type-level arithmetic, so the error tells you which numeric expression failed rather than listing every impl that was tried.

Whether you need it depends on how much type-level arithmetic you write. A crate with two or three type-level expressions will read its errors fine. A crate where the type level is doing real work, with nested sums and exponents in every signature, will spend a lot of time reading compiler output, and tnfilt is the answer the project points to.

There is also a directory in the repository called generate/ and a CHANGELOG.md, and the documentation for the full API is on docs.rs rather than in the repository, so the readme is a summary rather than a reference.

Where typenum is the wrong choice

The first limitation is compile time. Every type-level expression is a trait resolution the compiler has to do, and the arithmetic is on types rather than values, so a deeply nested expression produces a resolution problem rather than an arithmetic one. For a crate with heavy type-level arithmetic, the build gets slower and the errors get worse, and that is the trade the design makes explicit by shipping a diagnostic filter.

The second limitation is that the value-level world already has an answer for some of this. A const generic parameter carries a number in the type system with far less machinery, and where the number only needs to be known at compile time and not manipulated arithmetically, const generics are simpler. Typenum's advantage is the arithmetic, the ability to write Sum and Exp and to have the result usable as another length, and the ability to compute a length from a formula rather than restating it.

The third limitation is the incomplete type-level array. The manifest says it plainly, and it is the piece of the API most likely to bite a design that depends on it.

The real alternative, and the difference in approach is instructive, is the crates that build on typenum. The README names dimensioned, which does compile-time type checking for arbitrary unit systems, and generic-array, which provides arrays whose length you can generically refer to. Both use typenum rather than reimplementing it, and that is the point about the library's place in the ecosystem. It is a foundation layer, not an end user tool.

So the honest dividing line is this. If you are writing a library that needs a length, a bit width or a unit quantity as part of a type, and the arithmetic between those numbers matters, typenum is the established answer and building on it is cheaper than the alternatives. If you are writing an application, or a library that only needs a compile-time constant, const generics and a plain associated constant will do, and the type-level route buys you complexity you do not need.

Editorial conclusion

Adopt typenum if you are writing a Rust crate where a length, a bit width or a unit quantity has to be part of the type rather than a runtime value, since it gives you arithmetic on those numbers with no runtime cost and no dependency weight. Do not adopt it if a const generic or a plain associated constant would do, because the compile times and the error messages are the price of the type-level route. Before you commit, try your actual expression in a scratch project and see how long the compile takes, and check the strict feature, since that is what turns on the extra checking, and read the error output through tnfilt rather than by eye.

Frequently asked questions

What is typenum?

It is a Rust library for type-level numbers evaluated at compile time, currently supporting bits, unsigned integers and signed integers. It depends only on libcore, so it is suitable for use on any platform, and it is categorised as no-std in the package manifest.

How do I do arithmetic on typenum type-level numbers?

You combine the type-level number types through traits such as Sum and Exp, then read the result back with the Integer trait. The README example computes type X = Sum<P3, P4> and checks it equals 7, and type Y = Exp<N2, P3> and checks it equals -8.

How do I import typenum?

The library is divided into several modules but they are all re-exported through the crate root, so you can import anything with use typenum::whatever; and ignore the crate structure. The consts module can also be treated as a prelude with a glob import.

What do the typenum feature flags do?

There are five: const-generics, which lets the crate work with the compiler's const generic machinery, i128, which extends the supported integer range to 128 bits, strict, which adds extra checks and is what the project's own test and documentation runs use, scale_info, which enables the derive on the scale-info dependency, and no-std is the crate category.

How do I read typenum error messages?

The README is candid that typenum's error messages are not great and can be difficult to parse, and it points at tnfilt, a tool from Auxon that filters the compiler diagnostics down to the relevant part. The full API documentation is on docs.rs rather than in the repository.

Official sources

  1. Issues
  2. paholg/typenum on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/paholg-typenum.svg)](https://hysenlabs.com/projects/paholg-typenum)

Was this useful?