NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
crates.io · #2493 most downloaded on crates.io
generate typescript bindings from rust types
Last release 8 months ago
31 Jan 2026
Ships fairly regularly
a new release about every 4 months
Rarely documented
notes for 10 of 49 stable releases
Nothing withdrawn
no release was ever pulled
6 years old
49 releases · first in 2020
One column per quarter.
Nothing published for this version
Hello again! Today, we're excited to announce v12 of ts-rs! 🥳
Hello again! Today, we're excited to announce v12 of ts-rs! 🥳
While this is a breaking release, we expect a seamless upgrade for most users.
Even though we adjusted the representation of two types, their new and improved bindings should be less restrictive in most cases and therefore require minimal intervention.
Besides that, only code that interacts directly with the TS trait should be affected and easily fixed.
HashMap & friends (again)With v12, we once again changed how HashMap, BTreeMap, etc. are represented in TypeScript.
As a result, bindings are more flexible, work in more scenarios, and better match the expectations of TypeScript developers.
With this change, HashMap<K, V> will result in { [key in K]: V }.
The exception to this is when K is an enum - only then we generate { [key in K]?: V } instead.
Before, indexing into an object required dealing with undefined values. With this change, this is no longer forced onto users, which is the expected behavior for most TypeScript developers. If you want tsc to be pedantic about undefined values, enable noUncheckedIndexedAccess, which exists for this very purpose.
To aid migration, you can set the TS_RS_USE_V11_HASHMAP environment variable to revert to the previous behavior. However, we do intend on removing this flag in a future release.
By default, large integers that don't fit into a 64-bit float are exported as bigint.
With this release, this has now become configurable through environment variables and/or .cargo/config.toml.
Using environment variables, the generation and exporting of bindings can be configured. With v12, we now allow for the same level of control when exporting bindings programmatically.
With this release, we altered the representation of unit structs and unit variants from Record<string, never> to Record<symbol, never>.
It is equally expressive, but avoids a quirk in the TS typesystem when contained within an enum variant.
arrayvec cratejiff crate#[serde(crate = ...)] by @gustavo-shigueo in #447jiff v0.2 by @demhadais in #458#[ts(optional)] on an Option<Generic> by @gustavo-shigueo in #454Full Changelog: v11.1.0...v12.0.0
Record<symbol, never> (#431)HashMap to { [key in K]: V } if K is not an enum (#446)TS_RS_LARGE_INT environment variable to configure binding for i64, u64, i128, etc. (#448)arrayvec (#469)jiff (#458)Note: With the introduction of this feature, we deprecate the import-esm cargo feature. It will be removed in a future major release.
Today, we're happy to publish a small follow-up to v11.0.1!
This release fixes a nasty build failure when using the format feature.
Note: For those that use the format feature, this release bumps the MSRV to 1.88. We'd have preferred to do this in a major release, but felt this was acceptable since the build was broken by one of the dependencies anyway.
#[ts(repr(enum))#[ts(repr(enum)) instructs ts-rs to generate an enum, instead of a type for your rust enum.
#[derive(TS)]
#[ts(repr(enum))]
enum Role {
User,
Admin,
}
// will generate `export enum Role { "User", "Admin" }`Discriminants are preserved, and you can use the variant's name as discriminant instead using #[ts(repr(enum = name))]
#[ts(optional_fields)] in enumsThe #[ts(optional_fields)] attribute can now be applied directly to enums, or even to individual enum variants.
Normally, we generate import { Type } from "file" statements. In some scenarios though, it might be necessary to use a .ts or even .js extension instead.
This is now possible by setting the TS_RS_IMPORT_EXTENSION environment variable.
Note: With the introduction of this feature, we deprecate the
import-esmcargo feature. It will be removed in a future major release.
#[ts(optional)] with #[ts(type)] by @NyxCode in #416rename_all compatible with tuple and unit structs as a no-op attribute by @gustavo-shigueo in #422import-esm with TS_RS_IMPORT_EXTENSION by @gustavo-shigueo in #423#[ts(repr(enum)] attribute by @gustavo-shigueo in #425format feature by @gustavo-shigueo in #438#[ts(repr(enum))] attribute (#425)#[ts(optional_fields)] in enums and enum variants (#432)import-esm cargo feature in favour of RS_RS_IMPORT_EXTENSION (#423)chrono::Duration (#434)Fix usage of #[ts(optional)] together with #[ts(type)].
#[ts(optional)] together with #[ts(type)]. (#416)We are excited to announce v11.0.0!
We are excited to announce v11.0.0!
v10.x.xserde-compat feature enabled (default), fields annotated with both #[serde(skip_serializing(_if))] and #[serde(default)] are now turned into optional properties.ts_rs::TS trait has slightly changed.ts_rs::TS directly.With v11, we introduce #[ts(optional_fields)], which can cut down on annoying boilerplace.
This attribute can be applied to structs and has the same effect as adding #[ts(optional)] to every field.
Example:
#[derive(TS)]
#[ts(optional_fields)]
struct Form {
first_name: Option<String>, // first_name?: string
last_name: Option<String>, // last_name?: string
email: Option<String>, // email?: string
}In the past, #[serde(skip_serializing)] and #[serde(skip_serializing_if = "..")] were ignored by ts-rs.
With v11, we now take these attributes into account, as long as they are used together with #[serde(default)].
This ensures that the generated type is valid for both serializing and deserializing rust structures by default.
A field annotated with #[serde(skip_serializing_(if))] and #[serde(default)] will be treated as if it was annotated with #[ts(optional = nullable)].
This behavior can be overridden using #[ts(optional = false)].
Example:
// now generates `type User = { nickname?: string | null }`,
// making it correct for both serialization and deserialization by default.
#[derive(Serialize, Deserialize, TS)]
struct User {
#[serde(skip_serializing_if = "Option::is_none", default)]
nickname: Option<String>,
}#[doc = ..], #[ts(rename = "..")] and #[ts(export_to = "..")] now accept arbitrary expressions!
This enables some cool new patterns and makes integrating ts-rs in unusual setups easier.
Example:
// Renamed to the name of the current module
#[derive(TS)]
#[ts(rename = module_path!().rsplit_once("::").unwrap().1)]
struct Model;
// Comment containing the file path where the type was defined
#[derive(TS)]
#[doc = concat!("Defined in ", file!())]
struct UserGroup { . }The #[ts(optional)] attribute can now also be applied to fields of tuple structs.
Example:
// generates `type Location = [Country, State, City?]`
#[derive(TS)]
struct Location(Country, State, #[ts(optional)] City);#[ts(optional)] to struct by @gustavo-shigueo in #366#[ts(as = "...")] dependency list by @gustavo-shigueo in #385#[ts(rename)] by @NyxCode in #398#[ts(export_to)] by @NyxCode in #399#[doc] by @NyxCode in #411#[ts(flatten)] for fields of type HashMap by @gustavo-shigueo in #406serde(skip_serializing_if) + serde(default) by @manifest in #393#[serde(skip_serializing)] and #[serde(skip_serializing_if = ..)] are no longer ignored when used together with
#[serde(default)]. (#393)TS::output_path() from Option<&'static Path> to Option<PathBuf>.
This will only break your code if you manually implement TS or directly interact with the TS trait.TS::DOCS with TS::docs().
This will only break your code if you manually implement TS or directly interact with the TS trait.OptionInnerType associated type to the TS trait. If you manually implement TS, you must set this associated type to Self in all of your implementations.1.78.0 due to use of #[diagnostic::on_unimplemented] and let ... else { ... }#[serde(skip_serializing)] and #[serde(skip_serializing_if = ..)] when used together with
#[serde(default)]. Since these fields might be absent(#393)#[doc = concat!("defined in ", file!())].
This would result both in a rustdoc and JSDoc comment.#[ts(rename)] attribute on structs, enums and variants now accepts any expression.
This makes it possible to, for example, rename a struct to the name of a module it is contained in using #[ts(rename = module_path!().rsplit_once("::").unwrap().1)]#[ts(export_to)] attribute on structs and enums now accepts any expression.#[ts(optional_fields)] and #[ts(optional_fields = nullable)] attribute to structs, this attribute is equivalent to using the corresponding #[ts(optional)] or #[ts(optional = nullable)] on every field of the struct. (#366)v10.1 is a small follow-up to v10, bringing some bug-fixes and support for tokio .
v10.1 is a small follow-up to v10, bringing some bug-fixes and support for tokio.
tokio-impl feature gate)null in serde_json::Value by @NyxCode in #359cargo test export_bindings to README by @dessalines in #363#[ts(rename_all_fields)] by @sebastinez in #368Full Changelog: v10.0.0...v10.1.0
tokio (feature tokio-impl)serde_json::ValueWhile v10.0.0 is a technically breaking change, we expect it to be a drop-in replacement for almost all users .
While v10.0.0 is a technically breaking change, we expect it to be a drop-in replacement for almost all users.
HashMap<K, V> (& friends)In this release, we've changed how HashMap<K, V> is represented in TypeScript.
Before v10, ts-rs generated { [key: K]: V }. This was never technically correct, resulting in tsc accepting some code which it should not have. Additionally, this resulted in issues when e.g trying to use an enum as key.
With v10, we now generate { [key in K]?: V } instead.
#[ts(export_to = "..")]#[ts(as = "..")] and #[ts(type = "..")] now also work on enum variantsbson, smol_str)HashMap to export mapped types by @gustavo-shigueo in #339#[ts(export_to = "...")] to the same file by @escritorio-gustavo in #316#[ts(as = "...")] and #[ts(type = "...")] on enum variants by @escritorio-gustavo in #284HashMap<K, V> is represented in TypeScript. The resulting bindings ({ [key in K]?: V } instead of { [key: K]: V }) are more accurate and flexible.#[ts(export_to = "...")] attribute and be exported to the same file (#316)bson-uuid-impl feature now supports bson::oid::ObjectId as well (#340)smol_str behind cargo feature smol_str-impl (#350)#[ts(as = "...")] and #[ts(type = "...")] on enum variants (#384)This is a small patch release fixing a single bug:
#[ts(flatten)] on fields using generic parameters (#336)While v9.0.0 is a technically breaking change, we expect it to be a drop-in replacement for almost all users . Only code interacting with TS::dependen…
While v9.0.0 is a technically breaking change, we expect it to be a drop-in replacement for almost all users.
Only code interacting with TS::dependency_types and TS::generics will need to be adjusted.
TypeListThe biggest change of v9.0.0 is an internal one: We removed TypeList from the API.
This fixes the long-standing issue of complex types failing to compile with
overflow evaluating the requirement orreached the recursion limitEven if you did not run into those, we do expect this change to also improve compilation times.
_ in #[ts(as = "..")]Similar to how it works in serde, _ can be used in #[ts(as = "..")] to refer to the type of the field.
This is particularly useful for more complex type overrides.
#[ts(as = "..")] and #[ts(type = "..")] on structs and enumsThese two attributes can now be used directly on structs and enums.
Previously, it was necessary to add these attributes on every field where the type was used.
This feature is particularly useful for exposing newtypes transparently.
To see a list of all changes, check out CHANGELOG.md!
#[ts(rename_all_fields = "...")] on enums containing tuple or unit variants by @escritorio-gustavo in #287as by @dr-bonez in #288Dependencies from being added to dependency_types by @escritorio-gustavo in #291serde and add SCREAMING-KEBAB-CASE by @escritorio-gustavo in #298#[serde(with = "...")] for struct fields by @escritorio-gustavo in #280_) in #[ts(as = "...")] by @escritorio-gustavo in #299Cargo.lock files to improve CI speed by @escritorio-gustavo in #295Full Changelog: v8.1.0...v9.0.0
#[serde(with = "...")] requires the use of #[ts(as = "...")] or #[ts(type = "...")] (#280)snake_case, kebab-case and SCREAMING_SNAKE_CASE (#298)#[ts(rename_all = "...")] no longer accepts variations in the string's casing, dashes and underscores to make behavior consistent with serde (#298)TypeList, and replace TS::dependency_types/TS::generics with TS::visit_dependencies/TS::visit_generics.
This finally resolves "overflow evaluating the requirement", "reached the recursion limit" errors.
Also, compile times should benefit. This is a technically breaking change for those interacting with the TS trait
directly. For those just using #[derive(TS)] and #[ts(...)], nothing changes!#[ts(type = "..")] directly on structs and enums (#286)#[ts(as = "..")] directly on structs and enums (#288)#[ts(rename_all = "SCREAMING-KEBAB-CASE")] (#298)_ in #[ts(type = "..")] to refer to the type of the field (#299)#[ts(rename_all_fields = "...")] on enums containing tuple or unit variants (#287)TS_RS_EXPORT_DIR paths (#323)v8.1.0 is a mostly a small follow-up to v8.0.0, fixing a couple of rough edges.
v8.1.0 is a mostly a small follow-up to v8.0.0, fixing a couple of rough edges.
We expect v8.1.0 to be fully compatible to v8.0.0.
Additionally, we've added support for serde_json behind the serde-json-impl cargo feature.
This might seem like a small change, but a lot of work over the past months has now paid off and made cleanly supporting serde_json::Value possible.
#[ts(crate = "..")] to allow usage of #[derive(TS)] from other proc-macro crates (#274)serde_json behind cargo feature serde-json-impl (#276)HashMap and similar types are now represented as { [key: K]: V } instead of Record<K, V> (#277)TS trait in scope (#281)Full Changelog: v8.0.0...v8.1.0
#[ts(crate = "..")] to allow usage of #[derive(TS)] from other proc-macro crates (#274)serde_json behind cargo feature serde-json-impl (#276)After a lot of work and quite some time, we're happy to announce v8.0.0 of ts-rs . 🥳
After a lot of work and quite some time, we're happy to announce v8.0.0 of ts-rs. 🥳
7.x.xWhile this is a major release, we do expect that it's drop-in upgrade for most usecases.
However, if your setup is more involved (e.g interacting with the TS trait directly, doing post-processing on the output, etc.), some changes might be necessary.
If you're having any trouble migrating, please feel free to open a discussion or issue, we're happy to help.
#[ts(export)], all of its dependencies will be exported automatically, even if they are not annotated with #[ts(export)]. This behaviour is more convenient and always results in correct import statements.cargo test now exports all dependencies of types annotated with #[ts(export)]. The output directory can now be customized by setting the TS_RS_EXPORT_DIR environment variable.#[ts(export)] is not sufficient, we've reworked the API for exporting types from code.We've had a lot of new contributors since the last release!
I'm also excited to welcome @escritorio-gustavo as a new maintainer, who has been instrumental in making 8.0.0 happen!
type instead of ìnterface (#203)#[ts(export)], add TS::dependency_types() (#221)#[ts(skip)]TS::dependency_types() (#221)TS::generics() (#241)TS::WithoutGenerics (#241)TS::transparent() (#243)#[ts(export_to = "...")] are now relative to TS_RS_EXPORT_DIR, which defaults to ./bindings/TS::export with TS::export, TS::export_all and TS::export_to_all (#263)#[ts(as = "..")] (#174)Array<T> (#209)#[ts(optional = nullable)] (#213)semver-impl cargo feature with support for the semver crate (#176)HashMap with custom hashers (#173)import-esm cargo feature to import files with a .js extension (#192)#[ts(...)] equivalents for #[serde(tag = "...")], #[serde(tag = "...", content = "...")] and #[serde(untagged)] (#227)#[serde(untagged)] on individual enum variants (#226)#[serde(rename_all_fields = "...")] (#225)Result, Option, HashMap and Vec had their implementations of TS changed (#241)#[ts(...)] equivalent for #[serde(tag = "...")] being used on a struct with named fields (#244)#[ts(concrete(..))] to specify a concrete type for a generic parameter (#264)#[ts(bound = "...")] to manually override the generated where clause (#269)#[ts(skip)] and #[serde(skip)] in variants of adjacently or internally tagged enums (#231)rename_all with camelCase produces wrong names if fields were already in camelCase (#198)Full Changelog: v7.1.1...v8.0.0
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →