aleph-alpha / ts-rs Goto Github PK
View Code? Open in Web Editor NEWGenerate TypeScript bindings from Rust types
License: MIT License
Generate TypeScript bindings from Rust types
License: MIT License
Currently, having multiple types with the same name is problematic.
Firstly, with just #[ts(export)]
, they will just overwrite eachother. This can be worked around with #[ts(export_to = "..")]
.
Then, when a third type depends on two types with the same name, the generated imports for the third type will be invalid. This can be worked around with #[ts(rename = "..")]
.
How can we improve on this? Resolving these conflicts would require coordination between different invocations of derive(TS)
.
I don't like to reintroduce something like export!
to which you pass each type you wish to export, since this gets really painfull when working with a deep module tree, requiring you to make everything public.
I'm using usize rigorously throughout my codebase and I get the following compile-time error:
error[E0277]: the trait bound `usize: TS` is not satisfied
I tried this out but it doesn't work. Maybe you missed something out in the documentation but I could not get this to work in the short amount of time I wanted to. I did it manually.
In the future it would be cool if this could work straightforward.
Externally tagged unions
it the default for serde enums in e.g. JSON.
An enum like this:
pub enum Field {
Floor,
Wall,
Portal { id: usize, target: MapAddress },
Exit,
Special { id: usize },
}
would convert to this:
export type Field =
| "Floor"
| "Wall"
| {
Portal: {
id: number;
target: MapAddress;
};
}
| "Exit"
| {
Special: {
id: number;
};
};
It looks kinda ugly but it's very useful to me and it's the serde default.
this might be too big of an ask... but would be cool =)
The automatic imports should be import type { Foo } from ...
, otherwise strict typescript settings will cause the following error: import is never used as a value
.
Make formatting the generated ts bindings optional through a feature flag.
#[derive(Serialize, TS)]
struct User {
user_id: i32,
first_name: String,
last_name: String,
role: Role,
family: Vec<User>,
gender: Gender,
childrens: [User; 3],
}
I think that any exports within the same module should be combined into a single file, rather than splitting them up into separate files. I feel as though this should be the default, but perhaps a directive at the top of the module could indicate whether or not this happens?
Sometimes it might not be possible or desirable to generate full typings, eg when using third party definitions.
It would be great to have a #[ts(any)]
attribute that will just generate defintions with any
.
Should be available on struct fields and and variants.
When Rust's enums could be represented with TypeScript's enums I think they should. So for example an enum like:
#[derive(serde::Serialize, ts_rs::TS)
enum Color {
#[serde(rename="R")
Red,
Blue,
Green
}
should end up as
export enum Color {
Red="R",
Blue,
Green
}
Maybe even const enum
? Should probably be optional with an attribute on the enum.
Instead of currently it being: export type Color = "R" | "Blue" | "Green"
;
Hi !
I followed-up the recent updates on ts-rs
and was excited to give a try again (especially since the support for optional ?
-annotated fields, e.g.my_field?: string
, thanks @arendjr !).
I updated ts-rs
like :
ts-rs = { version = "3.0", features = ["serde-compat"] }
But then I got the following issue :
the package `***` depends on `ts-rs`, with features: `serde-compat` but `ts-rs` does not have these features.
I checked on docs.rs/ts-rs in the feature flags section, couldn't find any mention of it and thought like ok no worries it's probably just pulled by ts-rs-macros
internally.
So I gave a try like this :
use chrono::{DateTime, Utc};
use diesel::{Identifiable, Queryable};
use serde::Deserialize;
use ts_rs::{TS};
use crate::schema::database_table;
#[derive(TS)]
#[derive(Debug, Deserialize, Identifiable, Queryable)]
#[table_name = "database_table"]
pub struct MyStruct {
pub id: i32,
pub name: String,
#[ts(type = "string")] // we deserialize to RFC-3339 string representation
pub created_at: DateTime<Utc>,
#[ts(type = "string")]
#[serde(skip_serializing_if="Option::is_none")]
pub updated_at: Option<DateTime<Utc>>,
}
omitting the
export! { ... }
because I do it at the root of the module
Which generated this result :
export interface MyStruct {
id: number;
name: string;
created_at: string;
updated_at: string; // please note here I was expecting updated_at?: string
}
As a side note I also found mention of a chrono-impl
feature flag, but here again, the compiler told me that it couldn't be found.
Well, mistake might be on me, some help would be much appreciated :)
Hello,
as you're aware, hiding the code generation in testcases is a bit of a hack and probably annoying for anyone actually using their tests for other matters. Like testing.
If you haven't seen it, https://github.com/Wulf/tsync is taking a different approach: Instead of generating a meaningful #[derive(TS)]
at build time, it will parse your source code independently of the compiler. But both tsync and ts-rs use the syn
crate, and will parse a syn::Item::Struct
/::Enum
into type information.
Their approach has a few advantages:
#[derive(..)]
(though that is probably minor)But let's not hide the disadvantages:
tsync
binary. This could be avoided by letting users add a one-liner [[bin]]
or [[example]]
to their project and calling that. (an example has the advantage that you can put its deps into dev-dependencies)I was wondering if it'd be useful to better abstract the type parsing away from the type consumption. We create a function taking a syn::Item
and returning a struct DerivedTS
(or something more generic than it).
Using that function, we could implement both the actual #[derive(TS)]
and testcase generation, but we could also implement an external parser similar to tsync for projects where that's a better fit.
Do you think that'd be useful? Am I overlooking something important? Is this something I should hack on the next slow weekend, or over the holidays?
When dealing with javascript code bases, it is sometimes useful to use typescript definition files even when you are not using typescript. Currently, the way the types are declared, they are export
ed, meaning you must import them, like such
/** @type {import("../types/generated/ingest.js").TelemetryPacket} */
where as if they were declared
ambiently, they would not need to be imported. This is common practice in typescript declaration files for non-modules. Below are examples of exports vs declarations.
This could be achieved through an attribute to the derive macro.
Currently empty structs are represented as type Foo = null
. A reasonable alternative would be interface Foo = {}
.
Currently, ts-rs relies on the ordering of rust's TypeId to write out imports. However, the rust documentation specifies:
While TypeId implements Hash, PartialOrd, and Ord, it is worth noting that the hashes and ordering will vary between Rust releases. Beware of relying on them inside of your code!
This means that the ordering of imports may change from time to time. Especially when using version control, this causes unnecessary changes.
I would recommend sorting by name instead.
Hello,
I noticed that a struct with raw properties (e.g. r#type
) like these:
#[derive(TS)]
struct Test {
r#type: String,
}
results in:
thread '...::export_typescript' panicked at 'could not format output: "Line 2, column 8: Expected {, got interface\n\n export interface Test {\n ~~~~~~~~~"', ...\mod.rs:38:1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Hi,
Currently deriving for types that have fields like DateTime<Utc>
which have TS impls that do not require the generic parameter to implement TS is broken because the derive macro generates code that requires Utc to implement TS. For example
use ts_rs::TS;
struct Generic<T> {
t: T
}
impl<T: 'static> TS for Generic<T> {
fn name() -> String {
"string".to_string()
}
fn dependencies() -> Vec<(std::any::TypeId, String)> {
vec![]
}
fn transparent() -> bool {
false
}
}
struct Struct;
#[derive(TS)]
struct ContainsGenerics {
g: Generic<Struct>,
}
gives
error[E0277]: the trait bound `Struct: TS` is not satisfied
--> src/main.rs:23:10
|
23 | #[derive(TS)]
| ^^ the trait `TS` is not implemented for `Struct`
|
note: required by `name`
As far as I can tell the problem is the special handling done in types::generics::format_type
. The derive macro should rely on the TS impl of DateTime without processing the generic parameter.
rust's i64::MAX == 9223372036854775807
(2^63 - 1) while in javascript Number.MAX_SAFE_INTEGER == 9007199254740991
(2^53 - 1)
But in this library, only i128
and u128
is defined using BigInt
. So I was wondering if there was some reasoning behind the decision.
Import statements are working for most of my components, but some reason not for this one:
use serde::Serialize;
use ts_rs::TS;
/// A single attribute belonging to a particular component.
#[derive(Serialize, TS)]
#[ts(export)]
pub struct Attribute {
/// The attribute name, e.g. 'position' or 'mass'.
name: String,
/// The width in number of f32 values.
width: usize,
}
/// The descriptor of a game component's memory information.
#[derive(Serialize, TS)]
#[ts(export)]
#[ts(rename_all="lowercase")]
#[serde(tag="type")]
pub enum Component {
Point {
attributes: Vec<Attribute>
},
Source {
attributes: Vec<Attribute>
},
}
// bindings/Attribute.ts
export interface Attribute {
name: string;
width: number;
}
// bindings/Component.ts
export type Component = { type: "point"; attributes: Array<Attribute> } | {
type: "source";
attributes: Array<Attribute>;
};
Splitting them into separate modules so that they are run as separate tests does not make a difference:
test memory::comps::attr::export_bindings_attribute ... ok
test memory::comps::comp::export_bindings_component ... ok
Adding a non-struct-type enum variant does do the trick:
/// The descriptor of a game component's memory information.
#[derive(Serialize, TS)]
#[ts(export)]
#[ts(rename_all="lowercase")]
#[serde(tag="type")]
pub enum Component {
Foo(Vec<Attribute>),
Point {
attributes: Vec<Attribute>
},
Source {
attributes: Vec<Attribute>
},
}
import type { Attribute } from "./Attribute";
export type Component = { type: "foo" } & Array<Attribute> | {
type: "point";
attributes: Array<Attribute>;
} | { type: "source"; attributes: Array<Attribute> };
Skipping this variant does not work:
// ...
#[ts(skip)]
Foo(Vec<Attribute>),
export type Component = { type: "point"; attributes: Array<Attribute> } | {
type: "source";
attributes: Array<Attribute>;
};
Removing #[ts(rename_all="lowercase")]
and #[serde(tag="type")]
also makes no difference.
Adding a non-vecetor field (i.e foo: Attribute
) does not work.
Creating a newtype and then inlining that does not do the trick:
#[derive(Serialize, TS)]
pub struct Attributes(Vec<Attributes>);
// ...
#[ts(inline)]
attributes: Attributes,
Creating a separate struct with the named field does work:
#[derive(Serialize, TS)]
#[ts(export)]
pub struct Attributes {
attributes: Vec<Attribute>,
}
/// The descriptor of a game component's memory information.
#[derive(Serialize, TS)]
#[ts(export)]
#[ts(rename_all="lowercase")]
#[serde(tag="type")]
pub enum Component {
#[ts(inline)]
Point(Attributes),
Source(Attributes),
}
// bindings/Component.ts
import type { Attributes } from "./Attributes";
export type Component =
| { type: "point" } & Attributes
| { type: "source" } & Attributes;
//bindings/Attributes.ts
import type { Attribute } from "./Attribute";
export interface Attributes {
attributes: Array<Attribute>;
}
And it technically works because the type is equivalentโฆ but it's ugly.
It seems to be possible to derive e.g. serde's Serialize
trait from a (newtype wrapper)[https://doc.rust-lang.org/book/ch19-04-advanced-types.html] around a struct which itself does not implement this trait.
I tried to do the same with deriving the TS
trait from this library. And in this case the compiler seems to complain about a missing trait on the wrapped type.
Is there anything we can do to make this work?
This problem occurs in a project where we have put the project's Rust types into their own crate, so that these types can be shared and used both from e.g. backend and frontend crates.
Minimal repro: https://github.com/FruitieX/ts-rs-repro
Here we have two crates app
and types
. types
contains a SharedType struct that's exported via ts-rs. app
uses this type as part of it's internal AppState type, which I also want to export to TS using ts-rs.
The typings can be generated by running cargo test
in the root of the repo. I've committed the generated bindings for convenience.
The TS typings get exported into app/bindings/AppState.ts
and types/bindings/SharedType.ts
respectively. However, app/bindings/AppState.ts
tries to import from the relative path ./SharedType
, which doesn't work since the actual working relative path would be ../../types/bindings/SharedType
.
I'm currently working around this by manually moving the generated bindings into a shared bindings
dir in the root of our project repo.
#[derive(TS)]
struct A<T> {
foo: String,
}
Results in the following error:
wrong number of type arguments: expected 1, found 0
expected 1 type argument
I suspect this error is triggered somewhere in the macro implementation, though I don't see where exactly. What would it take for generics to be supported? If I had a few pointers I might try to see if I could implement it myself.
I noticed a few issues with enum variants. For instance:
#[derive(Serialize, TS)]
#[serde(tag = "kind", content = "data")]
enum ComplexEnum {
A,
B { foo: String, bar: f64 },
W(SimpleEnum),
F { nested: SimpleEnum },
T(i32, SimpleEnum),
V(Vec<Series>),
}
generates the following type:
// issue 1: there is no import for the `SimpleEnum` type
export type ComplexEnum =
| { kind: "A"; data: null } // issue 2: IMO it would be better to omit the `data` property
| {
kind: "B";
data: {
foo: string;
bar: number;
};
}
| { kind: "W"; data: SimpleEnum }
| {
kind: "F";
data: {
nested: SimpleEnum;
};
}
| { kind: "T"; data: [number, SimpleEnum] }
| { kind: "V"; data: Array }; // issue 3: there is no type argument passed to `Array`
I have fixed this issues in my own fork with the following commit: arendjr@e7a87b1
Unfortunately, it would be a bit unwieldy to create a PR for this as this is built on top of both #15 and #18. This is just to let you know these issues are addressed and I can create a PR for it when the other PRs are merged.
I have a few free-form values that I store as serde_json::Value
types (they get stored as jsonb in my database).
It would be nice to support serde_json::Value
, I assume this would map to any
on the typescript side.
So let's say I have types with interdependencies spanning multiple files:
/foo.rs
#[derive(TS)]
struct Foo {}
/bar.rs
#[derive(TS)]
enum Bar {
Foo(Foo)
}
How can I generate ts bindings such that Foo
is in scope for Bar
?
It looks like I can't export them to the same file, since the export!
macro only works with types declared in the same file.
this would be great, so if I have type Foo = String
in Rust, and I use Foo
, it will also be Foo
on the TS side
Hello, at first place I would like to thank you for your work.
I would like to generate a strict typescript type from a primitive newtype.
#[derive(TS)]
pub struct FooId(usize);
#[derive(TS)]
pub struct BarId(usize);
The above rust code generates following typescript definition.
export type FooId = number;
export type BarId = number;
So, we can write following code without error.
const fooId: FooId = 0;
let barId: BarId = 1;
barId = fooId; // <- Ok
I would like to generate following definitions.
type Phantom<T, U extends string> = T & { [key in U]: never };
export type FooId = Phantom<number, 'FooId'>;
export type BarId = Phantom<number, 'BarId'>;
With such a type, we can make the following assignments an error.
This is very useful if you want to strictly distinguish between ids.
const fooId: FooId = 0;
let barId: BarId = 1;
barId = fooId; // <- error
What do you think about it?
Serde allows users to specify tags for the content and the tag of an enum.
For example,
#[derive(TS, Deserialize)]
#[serde(tag = "kind", content= "data")]
enum Biz {
Foo,
Bar
}
generates: type Biz = "Foo" | "Bar"
When really it should generate:
type Biz = {kind: "Foo", content: null} | {kind: "Bar", content: null}
The latter type is deserializable. The former is not.
I'm currently trying to apply the TS
derive to the following:
#[derive(Clone, PartialEq, Eq, Encode)]
- #[cfg_attr(feature = "std", derive(Decode, Serialize, Debug))]
+ #[cfg_attr(feature = "std", derive(Decode, Serialize, Debug, TS))]
#[cfg_attr(
feature = "std",
serde(bound(serialize = "T::Type: Serialize, T::String: Serialize"))
)]
pub struct StorageEntryMetadata<T: Form = MetaForm> {
pub name: T::String,
pub modifier: StorageEntryModifier,
pub ty: StorageEntryType<T>,
pub default: Vec<u8>,
pub docs: Vec<T::String>,
}
The resulting error points to the =
token:
expected one of `(`, `+`, `,`, `::`, `<`, or `>`, found `=`
Any advice on getting this working would be greatly appreciated. Thank you!
see #6
Example:
use ts_rs::TS;
use serde::Serialize;
#[derive(Serialize, TS)]
#[serde(tag = "type")]
pub enum Thing {
#[serde(rename_all = "camelCase")]
A {
display_name: Option<String>,
},
B (u8),
}
resulting warning:
warning: failed to parse serde attribute
|
| #[serde(rename_all = "camelCase")]
|
= note: ts-rs failed to parse this attribute. It will be ignored.
so the resulting variant does not have it's field-names renamed in camel casing.
This feature flag would allow types to be exported directly to wasm_bindgen's .d.ts files! Very useful.
Additionally, possibly these types can be auto cast to &JsValue
s using serde so they can be returned directly from functions.
I could not find a way to run the binding generation outside of a test context.
Personally I don't think running it as test is a good idea. I'd rather provide a CLI flag that runs the binding generation.
Anyways, I think it would make sense if this question is up to the user.
Or do I oversee something here?
Hey! I've encountered the issue, that due to ts-rs
does not support the serde crate
feature, the program fails to compile.
Here is an example:
#[derive(BorshDeserialize, BorshSerialize, Serialize, Deserialize, TS)]
#[serde(crate = "near_sdk::serde")]
#[ts(export)]
pub struct SomeStruct {
pub someProperty: Option<Base64VecU8>,
}
Error:
warning: failed to parse serde attribute
|
|
| #[serde(crate = "near_sdk::serde")]
| #[serde(crate = "near_sdk::serde")]
|
= note: ts-rs failed to parse this attribute. It will be ignored.
error[E0277]: the trait bound `near_sdk::json_types::Base64VecU8: ts_rs::TS` is not satisfied
Thanks!
I'm getting an error when using with a struct with NaiveDateTime
field, how would I go about that?
I have a large enum, and when this is exported as a .ts interface the imports are duplicated in multiple instances:
import type { TypedValue } from "./TypedValue";
import type { TimeUnit } from "./TimeUnit";
import type { TypedValue } from "./TypedValue";
import type { Floor } from "./Floor";
import type { UnitDivision } from "./UnitDivision";
import type { TransferFee } from "./TransferFee";
import type { AreaUnit } from "./AreaUnit";
import type { TypedValue } from "./TypedValue";
import type { EnergyUnit } from "./EnergyUnit";
import type { FloatRange } from "./FloatRange";
import type { CurrencyUnit } from "./CurrencyUnit";
import type { TypedValue } from "./TypedValue";
import type { UnitDivision } from "./UnitDivision";
import type { AssessedValue } from "./AssessedValue";
Is there any deduplication done on the imports?
ts-rs = {version = "4", features = ["chrono-impl", "uuid-impl"]}
And it says,
depends on `ts-rs`, with features: `uuid-impl` but `ts-rs` does not have these features.
the chorono-impl is working alright by itself.
Currently, inlining fails to take the used generic type argument into account:
struct X<T> {
t: T
}
struct Y {
#[ts(inline)]
x1: X<i32>,
#[ts(flatten)]
x2: X<i32>
}
Is it possible to add for nested structs like
struct Player {
role: Role
}
the feature to export to one file like
export! {
[Role, Player] => "player.ts",
}
Then it's a single file and ready to use after generation without needing to manually add import
statements.
Great project btw!
Hi,
currently the the TS::decl() generated for a type like
#[derive(serde::Serialize, TS)]
enum Enum {
A(&'static str),
B { s: &'static str },
C
}
looks like this
type Enum = string | {
s: string,
} | "C";
whereas serde_json generates output matching the type
type Enum = { "A": string } | { "B": { s: string } } | "C"
serde-compat
allows for matching other styles of representing enums, like #[serde(tag = "tag", content = "content")]
, but there's no way to match the default since there's no serde attribute for it. I think matching serde's default output is preferable since it allows for differentiating between A("C")
and C
, but an attribute would work too.
Most types are already covered by ts-rs, but the stdlib contains a few more ones that would be useful to have.
This is a list of what is included in serde by default, it would probably make sense to support most of these in ts-rs as well: https://docs.rs/serde/latest/serde/ser/index.html
Currently it is not possible to generate Typescript bindings for enums that contain data.
For example:
#[derive(TS)]
enum Foo {
Bar(String),
Other
}
Produces an error: "variant has data attached. Such enums are not yet supported."
In this case, we would expect typescript bindings like:
type Foo = string | "Other"
Minimal repro:
use std::collections::HashMap;
use ts_rs::TS;
type TypeAlias = HashMap<String, String>;
#[derive(TS)]
#[ts(export)]
enum Enum {
A(TypeAlias),
B(HashMap<String, String>),
}
#[derive(TS)]
#[ts(export)]
struct Struct {
a: TypeAlias,
b: HashMap<String, String>
}
fn main() {}
Generates:
export type Enum = { A: Record } | { B: Record<string, string> };
export interface Struct { a: Record, b: Record<string, string>, }
Expected both generated variants/fields to be identical.
Hi,
I noticed that the struct
#[derive(ts_rs::TS)]
struct Wrapper(Vec<String>);
results in
export type Wrapper = Array;
in 4.x. In 3.x the result was
export type Wrapper = string[];
I think the issue is the same as in #32 (comment), which is to say there's no method to call that will return Array<string>
that we could use at
ts-rs/macros/src/types/newtype.rs
Line 39 in 5e299d6
struct Wrapper(Vec<Vec<String>>)
.Thanks for this package, I'm finding it very useful for sharing a protocol between Rust and ts. I hate to create an issue instead of a PR, but I can't spare the time to make a PR right now. So here goes:
Cargo.toml:
[package]
name = "blah"
version = "0.1.0"
edition = "2018"
[dependencies]
ts-rs = "3.0.0"
src/main.rs:
use ts_rs::{TS, export};
#[derive(TS)]
#[ts(rename_all = "camelCase")]
struct User {
r#type: i32,
}
export! {
User => "bindings.ts"
}
Result bindings.ts:
export interface User {
rType: number;
}
Specifically, rType
in the output. I note that the bindings aren't generated at all without rename_all = "camelCase"
. The workaround I'm currently using is to use #[ts(rename = "type")]
on the relevant field.
Hi, I've just tried out your crate on different definitions and it looks very promising !
There's just a question on how to alternatively handle Option<T>
:
given this struct :
#[derive(Serialize, TS)]
struct CreateUser {
first_name: Option<String>,
last_name: Option<String>,
}
will currently generate :
export interface CreateUser {
first_name: string | null,
last_name: string | null,
}
would it be possible to instead generate :
export interface CreateUser {
first_name?: string,
last_name?: string,
}
I started looking at your lib sourcecode, but I might not have time to dig in, especially that I noticed on impl<T: TS> TS for Option that it is currently implemented on the generated type, and I'm not sure how to link it with the generated field.
Any feedback would be greatly appreciated !
Thanks :)
Example:
#[derive(ts_rs::TS)]
struct Foo {}
Compile error:
cannot infer type for type parameter `T`
Note that empty structs without braces work.
Currently, the documentation is pretty sparse and only lives in doc comments.
Something similar to the serde docs would be cool to properly document the various attributes.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.