-
Notifications
You must be signed in to change notification settings - Fork 116
Deriving the TS trait
The TS
trait can be easily derived through its #[derive(TS)]
macro,
which will automatically handle mapping a Rust type into TypeScript type
definitions.
The #[derive(TS)]
macro provides an attribute helper macro called #[ts(...)]
which can help you control how the types will be generated
These are attributes that can be used both with structs and enums
This attribute causes the generation of a test which will create a ts file
containing the TypeScript declaration for your type, as well as any types
it depends on (as long as they implement TS
)
Allows you to change where your TypeScript file will be generated.
The default is ./bindings/TypeName.ts
, this path is relative to Cargo.toml
.
Usage:
#[derive(ts_rs::TS)]
#[ts(export)]
struct MyStruct {
foo: String
}
This must be either:
- An absolute path
- A path relative to your crate's
Cargo.toml
The given path will be treated as a directory if it ends with a /
character, in which case a file called TypeName.ts
will be created
within the given directory, otherwise the path will be treated as a
file (even without an extension).
Usage:
#[derive(ts_rs::TS)]
#[ts(export, export_to = "../ts_project/bindings/")] // Note that #[ts(export)] is still required
struct MyStruct {
foo: String
}
If you see yourself using #[ts_export = "..."]
with the same directory
for a lot (or even all) your types, there is a more convenient way to do
this.
Create a directory at the root of your project (the directory that contains
Cargo.toml
) called .cargo
. Inside the .cargo
directory, create a file
called config.toml
and type the following:
[env]
TS_RS_EXPORT_DIR = { value = "...", relative = true }
Where value
is a path to a directory (i.e. it must end with a slash /
)
relative to your Cargo.toml
file.
Now, using #[ts(export)]
without #[ts(export_to = "...")]
will result
in exporting to the directory defined in your config.toml
file and if you
use #[ts(export_to = "...")]
, its path will be relative to that directory.
Changes the name of your type's TypeScript representation.
If the feature flag serde-compat
is enabled (default), using
#[serde(rename = "...")]
will have the same effect.
Usage:
#[derive(ts_rs::TS)]
#[ts(export, rename = "MyType")]
struct MyStruct {
foo: String
}
Generates:
export type MyType = { foo: string, };
Renames all the fields in your struct or variants in your enum to use a given inflection.
Accepted values are lowercase
, snake_case
, kebab-case
, UPPERCASE
,
camelCase
, PascalCase
and SCREAMING_SNAKE_CASE
.
If the feature flag serde-compat
is enabled (default), using
#[serde(rename_all = "...")]
will have the same effect.
Usage:
#[derive(ts_rs::TS)]
#[ts(export, rename_all = "camelCase")]
struct MyStruct {
foo_bar: String
}
Generates:
export type MyStruct = { fooBar: string, };
Add the struct's name (or value of #[ts(rename = "...")]
) as a field with the given key.
Usage:
#[derive(ts_rs::TS)]
#[ts(export, tag = "hello")]
struct MyStruct {
foo_bar: String
}
Generates:
export type MyStruct = { "hello": "MyStruct", fooBar: string, };
Inlines the type of this field, replacing its name with its definition.
Usage:
#[derive(TS)]
struct Pagination {
limit: u32,
offset: u32,
total: u32,
}
#[derive(TS)]
struct Users {
users: Vec<HashMap<String, String>>,
#[ts(inline)]
pagination: Pagination,
}
Generates:
export type Users = {
users: Array<Record<string, string>>;
pagination: {
limit: number;
offset: number;
total: number;
};
};
The #[ts(flatten)]
attribute inlines keys from a field into
the parent struct.
Any struct, enum or map may be flattened, but beware that you should not flatten an externally tagged enum that contains non-skipped unit variants, like the following:
#[derive(TS)]
// The absence of #[ts(tag = "...")], #[ts(tag = "...", content = "...")] or #[ts(untagged)]
// means this enum is externally tagged
enum MyEnum {
Foo, // Unit variant, #[ts(skip)] not used
Bar(u32)
}
#[derive(TS)]
struct MyStruct {
biz: i32,
#[ts(flatten)] // You should not do this
qux: MyEnum
}
Why? Well, this generates the following TS code:
export type MyStruct = { biz: number, } & ("Foo" | { "Bar": number })
So in case of the Foo
variant, your type will be { biz: number } & 'Foo'
,
which doesn't make any sense.
Also note that this is valid (though heavily discouraged) if the flattened enum is the only field in your struct, because
#[derive(TS)]
struct MyStruct {
#[ts(flatten)] // You should still not do this, even though the TS is valid
qux: MyEnum
}
Generates:
export type MyStruct = "Foo" | { "Bar": number }
Which is valid TS, but serde will still fail to (de)serialize it
If the feature flag serde-compat
is enabled (default), using
#[serde(flatten)]
will have the same effect.
Usage:
#[derive(TS)]
struct Pagination {
limit: u32,
offset: u32,
total: u32,
}
#[derive(TS)]
struct Users {
users: Vec<HashMap<String, String>>,
#[ts(flatten)]
pagination: Pagination,
}
Generates:
export type Users = {
users: Array<Record<string, string>>;
limit: number;
offset: number;
total: number;
};
Say you are using a crate called foo
that has the
following struct:
// foo/lib.rs
pub Foo {
bar: u32
}
In your code, you wish to have something like:
#[derive(TS)]
struct MyCoolStruct {
my_field: foo::Foo, // Compiler error: foo::Foo doesn't implement `TS`
}
When you are using a type from an external crate that
does not implement TS
, it becomes impossible to use
this type normally as the type for a struct field, since
you cannot implement it yourself due to the orphan rule.
#[ts(as = "...")]
helps you solve that problem.
You will need to create a duplicate of foo::Foo
with
the same fields, which derives TS
, then use
#[ts(as = "...")]
to point to that struct:
#[derive(TS)]
pub FooDef {
bar: u32,
}
#[derive(TS)]
struct MyCoolStruct {
#[ts(as = "FooDef")]
my_field: foo::Foo,
}
Now, to generate the TypeScript code, ts_rs
will
use FooDef
instead of Foo
, avoiding the compiler
error.
Manually override the type emitted in the TypeScript code. This is generally not recommended unless you have no other option.
This also avoids the issue that #[ts(as = "...")]
solves, but at the cost of handwriting the TS type in a
string, potentially introducing TS syntax errors.
// foo/lib.rs
pub Foo {
bar: u32
}
#[derive(TS)]
struct MyCoolStruct {
#[ts(type = "{ bar: number }")]
my_field: foo::Foo,
#[ts(type = "string")]
other_field: u32,
#[ts(type = "{ bar; number }")]
// ^ This should be a colon
uh_oh: foo::Foo,
}
This will generate:
export type MyCoolStruct = {
my_field: { bar: number };
other_field: string;
uh_oh: { bar; number };
// ^ The contents of `#[ts(type = "...")]` are copied
// verbatim, so the syntax error is carried over
};
May be applied on a struct field of type Option<T>
. By default, such a field would turn into t: T | null
.
If #[ts(optional)]
is present, t?: T
is generated instead.
Usage:
#[derive(TS)]
#[ts(export)]
struct Foo {
#[ts(optional)]
bar: Option<u32>,
}
Generates
export type Foo = { bar?: number, };
May be applied on a struct field of type Option<T>
. By default, such a field would turn into t: T | null
.
If #[ts(optional = nullable)]
is present, t?: T | null
is generated.
Usage:
#[derive(TS)]
#[ts(export)]
struct Foo {
#[ts(optional = nullable)]
bar: Option<u32>,
}
Generates
export type Foo = { bar?: number | null, };
Avoids generating TS definitions for a field.
If the feature flag serde-compat
is enabled (default), using
#[serde(skip)]
will have the same effect.
Usage:
#[derive(TS)]
#[ts(export)]
struct Foo {
#[ts(skip)]
bar: u32,
baz: String,
}
Generates
export type Foo = { baz: string, };
Changes the name of the field in the type's TS representation.
If the feature flag serde-compat
is enabled (default), using
#[serde(rename = "...")]
will have the same effect.
Usage:
#[derive(TS)]
#[ts(export)]
struct Foo {
#[ts(rename = "biz")]
bar: u32,
}
Generates
export type Foo = { biz: number, };
By default, enum type definitions will match serde's externally tagged enums, which means
#[derive(TS)]
#[ts(export)]
enum Message {
Request { id: String, method: String, params: HashMap<String, String> },
Response { id: String, status: u8 },
}
Generates:
// Note: this code snipped has been formatted manually to facilitate readability
// The exported code is not guaranteed to match this formatting, even with the
// "format" feature flag
export type Message =
| { "Request": { id: string, method: string, params: Record<string, string> } }
| { "Response": { id: string, status: number } };
This behavior can be changed with the attributes #[ts(tag = "...")]
,
#[ts(tag = "...", content = "...")]
and #[ts(untagged)]
. Like other
attributes, when the serde-compat
feature is enabled, using the serde
version of these will also apply the behavior described below.
Generates TS types for a serde internally tagged enum (TS calls these "discriminated unions").
Beware that this type of enum may not contain tuple variants, as that will generate invalid
TypeScript, as well as cause a runtime panic!
when using serde.
Newtype variants also have the same issue, unless they are a new type over a struct with named
fields.
If the feature flag serde-compat
is enabled (default), using
#[serde(tag = "...")]
will have the same effect.
Usage:
#[derive(TS)]
#[ts(tag = "type")]
enum Message {
Request { id: String, method: String, params: HashMap<String, String> },
Response { id: String, status: u8 },
}
Geneates
// Note: this code snipped has been formatted manually to facilitate readability
// The exported code is not guaranteed to match this formatting, even with the
// "format" feature flag
export type Message =
| { "type": "Request", method: string, params: Record<string, string> }
| { "type": "Response", status: number };
Generates TS types for a serde adjacently tagged enum.
If the feature flag serde-compat
is enabled (default), using
#[serde(tag = "...", content = "...")]
will have the same effect.
Usage:
#[derive(TS)]
#[ts(tag = "t", content = "c")]
enum Block {
Para(Vec<String>),
Str(String),
}
Geneates
export type Block = { "t": "Para", "c": Array<string>, } | { "t": "Str", "c": string, };
Generates TS types for a serde untagged enum.
If the feature flag serde-compat
is enabled (default), using
#[serde(untagged)]
will have the same effect.
Usage:
#[derive(TS)]
#[ts(untagged)]
enum Message {
Request { id: String, method: String, params: HashMap<String, String> },
Response { id: String, status: u8 },
}
Geneates
export type Message = { id: string, method: string, params: Record<string, string>, } | { id: string, status: number, };
Equivalent to adding #[ts(rename_all = "..."]
to every variant.
Accepted values are lowercase
, snake_case
, kebab-case
, UPPERCASE
,
camelCase
, PascalCase
and SCREAMING_SNAKE_CASE
.
If the feature flag serde-compat
is enabled (default), using
#[serde(rename_all_fields = "...")]
will have the same effect.
Usage:
#[derive(TS)]
#[ts(export, rename_all_fields = "UPPERCASE")]
enum Message {
Request { id: String, method: String, params: HashMap<String, String> },
Response { id: String, status: u8 },
}
Generates:
// Note: this code snipped has been formatted manually to facilitate readability
// The exported code is not guaranteed to match this formatting, even with the
// "format" feature flag
export type Message =
| { "Request": { ID: string, METHOD: string, PARAMS: Record<string, string> } }
| { "Response": { ID: string, STATUS: number } };
Avoids generating TS definitions for a variant.
If the feature flag serde-compat
is enabled (default), using
#[serde(skip)]
will have the same effect.
Usage:
#[derive(TS)]
#[ts(export)]
enum Message {
#[ts(skip)]
Request { id: String, method: String, params: HashMap<String, String> },
Response { id: String, status: u8 },
}
Generates:
// Note: this code snipped has been formatted manually to facilitate readability
// The exported code is not guaranteed to match this formatting, even with the
// "format" feature flag
export type Message = { "Response": { id: string, status: number } };
Changes the name of a variant's tag (has no effect on untagged enums or variants).
If the feature flag serde-compat
is enabled (default), using
#[serde(rename = "...")]
will have the same effect.
Usage:
#[derive(TS)]
#[ts(export)]
enum Message {
#[ts(rename = "req")]
Request { id: String, method: String, params: HashMap<String, String> },
#[ts(rename = "res")]
Response { id: String, status: u8 },
}
Generates:
// Note: this code snipped has been formatted manually to facilitate readability
// The exported code is not guaranteed to match this formatting, even with the
// "format" feature flag
export type Message =
| { "req": { id: string, method: string, params: Record<string, string> } }
| { "res": { id: string, status: number } };
In an enum's struct variant, renames all the fields of the variant to use a given inflection.
Accepted values are lowercase
, snake_case
, kebab-case
, UPPERCASE
,
camelCase
, PascalCase
and SCREAMING_SNAKE_CASE
.
If the feature flag serde-compat
is enabled (default), using
#[serde(rename_all = "...")]
will have the same effect.
Usage:
#[derive(TS)]
#[ts(export)]
enum Message {
#[ts(rename_all = "UPPERCASE")]
Request { id: String, method: String, params: HashMap<String, String> },
Response { id: String, status: u8 },
}
Generates:
// Note: this code snipped has been formatted manually to facilitate readability
// The exported code is not guaranteed to match this formatting, even with the
// "format" feature flag
export type Message =
| { "Request": { ID: string, METHOD: string, PARAMS: Record<string, string> } }
| { "Response": { id: string, status: number } };
Allows specific enum variants to be treated as untagged, regardless
of the enums #[ts(tag = "...")]
or #[ts(tag = "...", content = "...")]
attribute.
If the feature flag serde-compat
is enabled (default), using
#[serde(untagged)]
will have the same effect.
Usage:
#[derive(TS)]
#[ts(export)]
enum Message {
Request { id: String, method: String, params: HashMap<String, String> },
#[ts(untagged)]
Response { id: String, status: u8 },
}
Generates:
// Note: this code snipped has been formatted manually to facilitate readability
// The exported code is not guaranteed to match this formatting, even with the
// "format" feature flag
export type Message =
| { "Request": { id: string, method: string, params: Record<string, string> } }
| { id: string, status: number };