Skip to main content

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;