ts_rs/lib.rs
1//! <h1 align="center" style="padding-top: 0; margin-top: 0;">
2//! <img width="150px" src="https://raw.githubusercontent.com/Aleph-Alpha/ts-rs/main/logo.png" alt="logo">
3//! <br/>
4//! ts-rs
5//! </h1>
6//! <p align="center">
7//! generate typescript interface/type declarations from rust types
8//! </p>
9//!
10//! <div align="center">
11//! <!-- Github Actions -->
12//! <img src="https://img.shields.io/github/workflow/status/Aleph-Alpha/ts-rs/Test?style=flat-square" alt="actions status" />
13//! <a href="https://crates.io/crates/ts-rs">
14//! <img src="https://img.shields.io/crates/v/ts-rs.svg?style=flat-square"
15//! alt="Crates.io version" />
16//! </a>
17//! <a href="https://docs.rs/ts-rs">
18//! <img src="https://img.shields.io/badge/docs-latest-blue.svg?style=flat-square"
19//! alt="docs.rs docs" />
20//! </a>
21//! <a href="https://crates.io/crates/ts-rs">
22//! <img src="https://img.shields.io/crates/d/ts-rs.svg?style=flat-square"
23//! alt="Download" />
24//! </a>
25//! </div>
26//!
27//! ## why?
28//! When building a web application in rust, data structures have to be shared between backend and frontend.
29//! Using this library, you can easily generate TypeScript bindings to your rust structs & enums so that you can keep your
30//! types in one place.
31//!
32//! ts-rs might also come in handy when working with webassembly.
33//!
34//! ## how?
35//! ts-rs exposes a single trait, `TS`. Using a derive macro, you can implement this interface for your types.
36//! Then, you can use this trait to obtain the TypeScript bindings.
37//! We recommend doing this in your tests.
38//! [See the example](https://github.com/Aleph-Alpha/ts-rs/blob/main/example/src/lib.rs) and [the docs](https://docs.rs/ts-rs/latest/ts_rs/).
39//!
40//! ## get started
41//! ```toml
42//! [dependencies]
43//! ts-rs = "7.1"
44//! ```
45//!
46//! ```rust
47//! use ts_rs::TS;
48//!
49//! #[derive(TS)]
50//! #[ts(export)]
51//! struct User {
52//! user_id: i32,
53//! first_name: String,
54//! last_name: String,
55//! }
56//! ```
57//! When running `cargo test`, the TypeScript bindings will be exported to the file `bindings/User.ts`.
58//!
59//! ## features
60//! - generate interface declarations from rust structs
61//! - generate union declarations from rust enums
62//! - inline types
63//! - flatten structs/interfaces
64//! - generate necessary imports when exporting to multiple files
65//! - serde compatibility
66//! - generic types
67//!
68//! ## limitations
69//! - generic fields cannot be inlined or flattened (#56)
70//! - type aliases must not alias generic types (#70)
71//!
72//! ## cargo features
73//! - `serde-compat` (default)
74//!
75//! Enable serde compatibility. See below for more info.
76//! - `format`
77//!
78//! When enabled, the generated typescript will be formatted.
79//! Currently, this sadly adds quite a bit of dependencies.
80//! - `chrono-impl`
81//!
82//! Implement `TS` for types from chrono
83//! - `bigdecimal-impl`
84//!
85//! Implement `TS` for types from bigdecimal
86//! - `url-impl`
87//!
88//! Implement `TS` for types from url
89//! - `uuid-impl`
90//!
91//! Implement `TS` for types from uuid
92//! - `bson-uuid-impl`
93//!
94//! Implement `TS` for types from bson
95//! - `bytes-impl`
96//!
97//! Implement `TS` for types from bytes
98//! - `indexmap-impl`
99//!
100//! Implement `TS` for `IndexMap` and `IndexSet` from indexmap
101//!
102//! - `ordered-float-impl`
103//!
104//! Implement `TS` for `OrderedFloat` from ordered_float
105//!
106//! - `heapless-impl`
107//!
108//! Implement `TS` for `Vec` from heapless
109//!
110//! - `no-serde-warnings`
111//!
112//! When `serde-compat` is enabled, warnings are printed during build if unsupported serde
113//! attributes are encountered. Enabling this feature silences these warnings.
114//!
115//!
116//! If there's a type you're dealing with which doesn't implement `TS`, use `#[ts(type = "..")]` or open a PR.
117//!
118//! ## serde compatability
119//! With the `serde-compat` feature (enabled by default), serde attributes can be parsed for enums and structs.
120//! Supported serde attributes:
121//! - `rename`
122//! - `rename-all`
123//! - `tag`
124//! - `content`
125//! - `untagged`
126//! - `skip`
127//! - `skip_serializing`
128//! - `skip_deserializing`
129//! - `skip_serializing_if = "Option::is_none"`
130//! - `flatten`
131//! - `default`
132//!
133//! When ts-rs encounters an unsupported serde attribute, a warning is emitted, unless the feature `no-serde-warnings` is enabled.
134//!
135//! ## contributing
136//! Contributions are always welcome!
137//! Feel free to open an issue, discuss using GitHub discussions or open a PR.
138//! [See CONTRIBUTING.md](https://github.com/Aleph-Alpha/ts-rs/blob/main/CONTRIBUTING.md)
139//!
140//! ## todo
141//! - [x] serde compatibility layer
142//! - [x] documentation
143//! - [x] use typescript types across files
144//! - [x] more enum representations
145//! - [x] generics
146//! - [ ] don't require `'static`
147
148use std::{
149 any::TypeId,
150 collections::{BTreeMap, BTreeSet, HashMap, HashSet},
151 net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
152 num::{
153 NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128,
154 NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize,
155 },
156 ops::{Range, RangeInclusive},
157 path::{Path, PathBuf},
158};
159
160pub use ts_rs_macros::TS;
161
162pub use crate::export::ExportError;
163
164#[cfg(feature = "chrono-impl")]
165mod chrono;
166mod export;
167
168/// A type which can be represented in TypeScript.
169/// Most of the time, you'd want to derive this trait instead of implementing it manually.
170/// ts-rs comes with implementations for all primitives, most collections, tuples,
171/// arrays and containers.
172///
173/// ### exporting
174/// Because Rusts procedural macros are evaluated before other compilation steps, TypeScript
175/// bindings cannot be exported during compile time.
176/// Bindings can be exported within a test, which ts-rs generates for you by adding `#[ts(export)]`
177/// to a type you wish to export to a file.
178/// If, for some reason, you need to do this during runtime, you can call [`TS::export`] yourself.
179///
180/// ### serde compatibility
181/// By default, the feature `serde-compat` is enabled.
182/// ts-rs then parses serde attributes and adjusts the generated typescript bindings accordingly.
183/// Not all serde attributes are supported yet - if you use an unsupported attribute, you'll see a
184/// warning.
185///
186/// ### container attributes
187/// attributes applicable for both structs and enums
188///
189/// - `#[ts(export)]`:
190/// Generates a test which will export the type, by default to `bindings/<name>.ts` when running
191/// `cargo test`
192///
193/// - `#[ts(export_to = "..")]`:
194/// Specifies where the type should be exported to. Defaults to `bindings/<name>.ts`.
195/// If the provided path ends in a trailing `/`, it is interpreted as a directory.
196/// Note that you need to add the `export` attribute as well, in order to generate a test which exports the type.
197///
198/// - `#[ts(rename = "..")]`:
199/// Sets the typescript name of the generated type
200///
201/// - `#[ts(rename_all = "..")]`:
202/// Rename all fields/variants of the type.
203/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
204///
205///
206/// ### struct field attributes
207///
208/// - `#[ts(type = "..")]`:
209/// Overrides the type used in TypeScript.
210/// This is useful when there's a type for which you cannot derive `TS`.
211///
212/// - `#[ts(rename = "..")]`:
213/// Renames this field
214///
215/// - `#[ts(inline)]`:
216/// Inlines the type of this field
217///
218/// - `#[ts(skip)]`:
219/// Skip this field
220///
221/// - `#[ts(optional)]`:
222/// Indicates the field may be omitted from the serialized struct
223///
224/// - `#[ts(flatten)]`:
225/// Flatten this field (only works if the field is a struct)
226///
227/// ### enum attributes
228///
229/// - `#[ts(tag = "..")]`:
230/// Changes the representation of the enum to store its tag in a separate field.
231/// See [the serde docs](https://serde.rs/enum-representations.html).
232///
233/// - `#[ts(content = "..")]`:
234/// Changes the representation of the enum to store its content in a separate field.
235/// See [the serde docs](https://serde.rs/enum-representations.html).
236///
237/// - `#[ts(untagged)]`:
238/// Changes the representation of the enum to not include its tag.
239/// See [the serde docs](https://serde.rs/enum-representations.html).
240///
241/// - `#[ts(rename_all = "..")]`:
242/// Rename all variants of this enum.
243/// Valid values are `lowercase`, `UPPERCASE`, `camelCase`, `snake_case`, `PascalCase`, `SCREAMING_SNAKE_CASE`, "kebab-case"
244///
245/// ### enum variant attributes
246///
247/// - `#[ts(rename = "..")]`:
248/// Renames this variant
249///
250/// - `#[ts(skip)]`:
251/// Skip this variant
252pub trait TS {
253 const EXPORT_TO: Option<&'static str> = None;
254
255 /// Declaration of this type, e.g. `interface User { user_id: number, ... }`.
256 /// This function will panic if the type has no declaration.
257 fn decl() -> String {
258 panic!("{} cannot be declared", Self::name());
259 }
260
261 /// Name of this type in TypeScript.
262 fn name() -> String;
263
264 /// Name of this type in TypeScript, with type arguments.
265 fn name_with_type_args(args: Vec<String>) -> String {
266 format!("{}<{}>", Self::name(), args.join(", "))
267 }
268
269 /// Formats this types definition in TypeScript, e.g `{ user_id: number }`.
270 /// This function will panic if the type cannot be inlined.
271 fn inline() -> String {
272 panic!("{} cannot be inlined", Self::name());
273 }
274
275 /// Flatten an type declaration.
276 /// This function will panic if the type cannot be flattened.
277 fn inline_flattened() -> String {
278 panic!("{} cannot be flattened", Self::name())
279 }
280
281 /// Information about types this type depends on.
282 /// This is used for resolving imports when exporting to a file.
283 fn dependencies() -> Vec<Dependency>
284 where
285 Self: 'static;
286
287 /// `true` if this is a transparent type, e.g tuples or a list.
288 /// This is used for resolving imports when using the `export!` macro.
289 fn transparent() -> bool;
290
291 /// Manually export this type to a file.
292 /// The output file can be specified by annotating the type with `#[ts(export_to = ".."]`.
293 /// By default, the filename will be derived from the types name.
294 ///
295 /// When a type is annotated with `#[ts(export)]`, it is exported automatically within a test.
296 /// This function is only usefull if you need to export the type outside of the context of a
297 /// test.
298 fn export() -> Result<(), ExportError>
299 where
300 Self: 'static,
301 {
302 export::export_type::<Self>()
303 }
304
305 /// Manually export this type to a file with a file with the specified path. This
306 /// function will ignore the `#[ts(export_to = "..)]` attribute.
307 fn export_to(path: impl AsRef<Path>) -> Result<(), ExportError>
308 where
309 Self: 'static,
310 {
311 export::export_type_to::<Self, _>(path)
312 }
313
314 /// Manually generate bindings for this type, returning a [`String`].
315 /// This function does not format the output, even if the `format` feature is enabled.
316 fn export_to_string() -> Result<String, ExportError>
317 where
318 Self: 'static,
319 {
320 export::export_type_to_string::<Self>()
321 }
322}
323
324/// A typescript type which is depended upon by other types.
325/// This information is required for generating the correct import statements.
326#[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
327pub struct Dependency {
328 /// Type ID of the rust type
329 pub type_id: TypeId,
330 /// Name of the type in TypeScript
331 pub ts_name: String,
332 /// Path to where the type would be exported. By default a filename is derived from the types
333 /// name, which can be customized with `#[ts(export_to = "..")]`.
334 pub exported_to: &'static str,
335}
336
337impl Dependency {
338 /// Constructs a [`Dependency`] from the given type `T`.
339 /// If `T` is not exportable (meaning `T::EXPORT_TO` is `None`), this function will return
340 /// `None`
341 pub fn from_ty<T: TS + 'static + ?Sized>() -> Option<Self> {
342 let exported_to = T::EXPORT_TO?;
343 Some(Dependency {
344 type_id: TypeId::of::<T>(),
345 ts_name: T::name(),
346 exported_to,
347 })
348 }
349}
350
351// generate impls for primitive types
352macro_rules! impl_primitives {
353 ($($($ty:ty),* => $l:literal),*) => { $($(
354 impl TS for $ty {
355 fn name() -> String { $l.to_owned() }
356 fn name_with_type_args(args: Vec<String>) -> String {
357 assert!(args.is_empty(), "called name_with_type_args on primitive");
358 $l.to_owned()
359 }
360 fn inline() -> String { $l.to_owned() }
361 fn dependencies() -> Vec<Dependency> { vec![] }
362 fn transparent() -> bool { false }
363 }
364 )*)* };
365}
366// generate impls for tuples
367macro_rules! impl_tuples {
368 ( impl $($i:ident),* ) => {
369 impl<$($i: TS),*> TS for ($($i,)*) {
370 fn name() -> String {
371 format!("[{}]", [$($i::name()),*].join(", "))
372 }
373 fn inline() -> String {
374 format!("[{}]", [$($i::inline()),*].join(", "))
375 }
376 fn dependencies() -> Vec<Dependency>
377 where
378 Self: 'static
379 {
380 [$( Dependency::from_ty::<$i>() ),*]
381 .into_iter()
382 .flatten()
383 .collect()
384 }
385 fn transparent() -> bool { true }
386 }
387 };
388 ( $i2:ident $(, $i:ident)* ) => {
389 impl_tuples!(impl $i2 $(, $i)* );
390 impl_tuples!($($i),*);
391 };
392 () => {};
393}
394
395// generate impls for wrapper types
396macro_rules! impl_wrapper {
397 ($($t:tt)*) => {
398 $($t)* {
399 fn name() -> String { T::name() }
400 fn name_with_type_args(mut args: Vec<String>) -> String {
401 assert_eq!(args.len(), 1);
402 args.remove(0)
403 }
404 fn inline() -> String { T::inline() }
405 fn inline_flattened() -> String { T::inline_flattened() }
406 fn dependencies() -> Vec<Dependency>
407 where
408 Self: 'static
409 {
410 T::dependencies()
411 }
412 fn transparent() -> bool { T::transparent() }
413 }
414 };
415}
416
417// implement TS for the $shadow, deferring to the impl $s
418macro_rules! impl_shadow {
419 (as $s:ty: $($impl:tt)*) => {
420 $($impl)* {
421 fn name() -> String { <$s>::name() }
422 fn name_with_type_args(args: Vec<String>) -> String { <$s>::name_with_type_args(args) }
423 fn inline() -> String { <$s>::inline() }
424 fn inline_flattened() -> String { <$s>::inline_flattened() }
425 fn dependencies() -> Vec<$crate::Dependency>
426 where
427 Self: 'static
428 {
429 <$s>::dependencies()
430 }
431 fn transparent() -> bool { <$s>::transparent() }
432 }
433 };
434}
435
436impl<T: TS> TS for Option<T> {
437 fn name() -> String {
438 unreachable!();
439 }
440
441 fn name_with_type_args(args: Vec<String>) -> String {
442 assert_eq!(
443 args.len(),
444 1,
445 "called Option::name_with_type_args with {} args",
446 args.len()
447 );
448 format!("{} | null", args[0])
449 }
450
451 fn inline() -> String {
452 format!("{} | null", T::inline())
453 }
454
455 fn dependencies() -> Vec<Dependency>
456 where
457 Self: 'static,
458 {
459 [Dependency::from_ty::<T>()].into_iter().flatten().collect()
460 }
461
462 fn transparent() -> bool {
463 true
464 }
465}
466
467impl<T: TS, E: TS> TS for Result<T, E> {
468 fn name() -> String {
469 unreachable!();
470 }
471 fn inline() -> String {
472 format!("{{ Ok : {} }} | {{ Err : {} }}", T::inline(), E::inline())
473 }
474 fn dependencies() -> Vec<Dependency>
475 where
476 Self: 'static,
477 {
478 [Dependency::from_ty::<T>(), Dependency::from_ty::<E>()]
479 .into_iter()
480 .flatten()
481 .collect()
482 }
483 fn transparent() -> bool {
484 true
485 }
486}
487
488impl<T: TS> TS for Vec<T> {
489 fn name() -> String {
490 "Array".to_owned()
491 }
492
493 fn name_with_type_args(args: Vec<String>) -> String {
494 assert_eq!(
495 args.len(),
496 1,
497 "called Vec::name_with_type_args with {} args",
498 args.len()
499 );
500 format!("Array<{}>", args[0])
501 }
502
503 fn inline() -> String {
504 format!("Array<{}>", T::inline())
505 }
506
507 fn dependencies() -> Vec<Dependency>
508 where
509 Self: 'static,
510 {
511 [Dependency::from_ty::<T>()].into_iter().flatten().collect()
512 }
513
514 fn transparent() -> bool {
515 true
516 }
517}
518
519impl<K: TS, V: TS> TS for HashMap<K, V> {
520 fn name() -> String {
521 "Record".to_owned()
522 }
523
524 fn name_with_type_args(args: Vec<String>) -> String {
525 assert_eq!(
526 args.len(),
527 2,
528 "called HashMap::name_with_type_args with {} args",
529 args.len()
530 );
531 format!("Record<{}, {}>", args[0], args[1])
532 }
533
534 fn inline() -> String {
535 format!("Record<{}, {}>", K::inline(), V::inline())
536 }
537
538 fn dependencies() -> Vec<Dependency>
539 where
540 Self: 'static,
541 {
542 [Dependency::from_ty::<K>(), Dependency::from_ty::<V>()]
543 .into_iter()
544 .flatten()
545 .collect()
546 }
547
548 fn transparent() -> bool {
549 true
550 }
551}
552
553impl<I: TS> TS for Range<I> {
554 fn name() -> String {
555 panic!("called Range::name - Did you use a type alias?")
556 }
557
558 fn name_with_type_args(args: Vec<String>) -> String {
559 assert_eq!(
560 args.len(),
561 1,
562 "called Range::name_with_type_args with {} args",
563 args.len()
564 );
565 format!("{{ start: {}, end: {}, }}", &args[0], &args[0])
566 }
567
568 fn dependencies() -> Vec<Dependency>
569 where
570 Self: 'static,
571 {
572 [Dependency::from_ty::<I>()].into_iter().flatten().collect()
573 }
574
575 fn transparent() -> bool {
576 true
577 }
578}
579
580impl<I: TS> TS for RangeInclusive<I> {
581 fn name() -> String {
582 panic!("called RangeInclusive::name - Did you use a type alias?")
583 }
584
585 fn name_with_type_args(args: Vec<String>) -> String {
586 assert_eq!(
587 args.len(),
588 1,
589 "called RangeInclusive::name_with_type_args with {} args",
590 args.len()
591 );
592 format!("{{ start: {}, end: {}, }}", &args[0], &args[0])
593 }
594
595 fn dependencies() -> Vec<Dependency>
596 where
597 Self: 'static,
598 {
599 [Dependency::from_ty::<I>()].into_iter().flatten().collect()
600 }
601
602 fn transparent() -> bool {
603 true
604 }
605}
606
607impl_shadow!(as T: impl<'a, T: TS + ?Sized> TS for &T);
608impl_shadow!(as Vec<T>: impl<T: TS> TS for HashSet<T>);
609impl_shadow!(as Vec<T>: impl<T: TS> TS for BTreeSet<T>);
610impl_shadow!(as HashMap<K, V>: impl<K: TS, V: TS> TS for BTreeMap<K, V>);
611impl_shadow!(as Vec<T>: impl<T: TS, const N: usize> TS for [T; N]);
612impl_shadow!(as Vec<T>: impl<T: TS> TS for [T]);
613
614impl_wrapper!(impl<T: TS + ?Sized> TS for Box<T>);
615impl_wrapper!(impl<T: TS + ?Sized> TS for std::sync::Arc<T>);
616impl_wrapper!(impl<T: TS + ?Sized> TS for std::rc::Rc<T>);
617impl_wrapper!(impl<'a, T: TS + ToOwned + ?Sized> TS for std::borrow::Cow<'a, T>);
618impl_wrapper!(impl<T: TS> TS for std::cell::Cell<T>);
619impl_wrapper!(impl<T: TS> TS for std::cell::RefCell<T>);
620impl_wrapper!(impl<T: TS> TS for std::sync::Mutex<T>);
621impl_wrapper!(impl<T: TS + ?Sized> TS for std::sync::Weak<T>);
622impl_wrapper!(impl<T: TS> TS for std::marker::PhantomData<T>);
623
624impl_tuples!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
625
626#[cfg(feature = "bigdecimal-impl")]
627impl_primitives! { bigdecimal::BigDecimal => "string" }
628
629#[cfg(feature = "uuid-impl")]
630impl_primitives! { uuid::Uuid => "string" }
631
632#[cfg(feature = "url-impl")]
633impl_primitives! { url::Url => "string" }
634
635#[cfg(feature = "ordered-float-impl")]
636impl_primitives! { ordered_float::OrderedFloat<f32> => "number" }
637
638#[cfg(feature = "ordered-float-impl")]
639impl_primitives! { ordered_float::OrderedFloat<f64> => "number" }
640
641#[cfg(feature = "bson-uuid-impl")]
642impl_primitives! { bson::Uuid => "string" }
643
644#[cfg(feature = "indexmap-impl")]
645impl_shadow!(as Vec<T>: impl<T: TS> TS for indexmap::IndexSet<T>);
646
647#[cfg(feature = "indexmap-impl")]
648impl_shadow!(as HashMap<K, V>: impl<K: TS, V: TS> TS for indexmap::IndexMap<K, V>);
649
650#[cfg(feature = "heapless-impl")]
651impl_shadow!(as Vec<T>: impl<T: TS, const N: usize> TS for heapless::Vec<T, N>);
652
653#[cfg(feature = "bytes-impl")]
654mod bytes {
655 use super::TS;
656
657 impl_shadow!(as Vec<u8>: impl TS for bytes::Bytes);
658 impl_shadow!(as Vec<u8>: impl TS for bytes::BytesMut);
659}
660
661impl_primitives! {
662 u8, i8, NonZeroU8, NonZeroI8,
663 u16, i16, NonZeroU16, NonZeroI16,
664 u32, i32, NonZeroU32, NonZeroI32,
665 usize, isize, NonZeroUsize, NonZeroIsize, f32, f64 => "number",
666 u64, i64, NonZeroU64, NonZeroI64,
667 u128, i128, NonZeroU128, NonZeroI128 => "bigint",
668 bool => "boolean",
669 char, Path, PathBuf, String, str,
670 Ipv4Addr, Ipv6Addr, IpAddr, SocketAddrV4, SocketAddrV6, SocketAddr => "string",
671 () => "null"
672}
673#[rustfmt::skip]
674pub(crate) use impl_primitives;