Repository: Aleph-Alpha/ts-rs
Pull request #128 (2022-10-24 03:44:01)

Alleviate 'static/Sized restrictions
Hi,
Thanks for your work on ts-rs.

Here's my take on alleviating some of the 'static and Sized related restrictions for TS.

- 'static: This PR would allow non-'static types to derive TS and use decl and such, only gating the dependencies/export* and other functions that actually need it behind the 'static requirement.
- Sized: Allows the T inside Box, Rc, Arc and Cow to be unsized, such as in `Cow<'a str>`.

This makes some of the generic bounds more complicated and may potentially cause some confusion as to why exporting a non-'static type that implements TS doesn't work, but it also makes the trait more flexible and I believe in general that bounds on methods and functions are better compared to bounds on traits and structs. For example, in my use case the export functionality is not needed so this PR would allow me to use `Cow<'a, str>` in fields with no negatives. Let me know what you think.


Patch:
diff --git a/macros/src/lib.rs b/macros/src/lib.rs
index 9de50c8..849c970 100644
--- a/macros/src/lib.rs
+++ b/macros/src/lib.rs
@@ -95,7 +95,10 @@ impl DerivedTS {
                     #inline
                 }
                 #inline_flattened
-                fn dependencies() -> Vec<ts_rs::Dependency> {
+                fn dependencies() -> Vec<ts_rs::Dependency>
+                where
+                    Self: 'static,
+                {
                     #dependencies
                 }
                 fn transparent() -> bool {
diff --git a/macros/src/types/generics.rs b/macros/src/types/generics.rs
index 3f90370..d73fb44 100644
--- a/macros/src/types/generics.rs
+++ b/macros/src/types/generics.rs
@@ -7,28 +7,32 @@ use syn::{
 use crate::{attr::StructAttr, deps::Dependencies};
 
 /// formats the generic arguments (like A, B in struct X<A, B>{..}) as "<X>" where x is a comma
-/// seperated list of generic arguments, or an empty string if there are no generics.
+/// seperated list of generic arguments, or an empty string if there are no type generics (lifetime/const generics are ignored).
 /// this expands to an expression which evaluates to a `String`.
 ///
 /// If a default type arg is encountered, it will be added to the dependencies.
 pub fn format_generics(deps: &mut Dependencies, generics: &Generics) -> TokenStream {
-    if generics.params.is_empty() {
+    let mut expanded_params = generics
+        .params
+        .iter()
+        .filter_map(|param| match param {
+            GenericParam::Type(type_param) => Some({
+                let ty = type_param.ident.to_string();
+                if let Some(default) = &type_param.default {
+                    let default = format_type(default, deps, generics);
+                    quote!(format!("{} = {}", #ty, #default))
+                } else {
+                    quote!(#ty.to_owned())
+                }
+            }),
+            _ => None,
+        })
+        .peekable();
+
+    if expanded_params.peek().is_none() {
         return quote!("");
     }
 
-    let expanded_params = generics.params.iter().filter_map(|param| match param {
-        GenericParam::Type(type_param) => Some({
-            let ty = type_param.ident.to_string();
-            if let Some(default) = &type_param.default {
-                let default = format_type(default, deps, generics);
-                quote!(format!("{} = {}", #ty, #default))
-            } else {
-                quote!(#ty.to_owned())
-            }
-        }),
-        _ => None,
-    });
-
     let comma_separated = quote!(vec![#(#expanded_params),*].join(", "));
     quote!(format!("<{}>", #comma_separated))
 }
diff --git a/ts-rs/src/export.rs b/ts-rs/src/export.rs
index ed968ed..18f962e 100644
--- a/ts-rs/src/export.rs
+++ b/ts-rs/src/export.rs
@@ -27,13 +27,15 @@ pub enum ExportError {
 }
 
 /// Export `T` to the file specified by the `#[ts(export_to = ..)]` attribute
-pub(crate) fn export_type<T: TS + ?Sized>() -> Result<(), ExportError> {
+pub(crate) fn export_type<T: TS + ?Sized + 'static>() -> Result<(), ExportError> {
     let path = output_path::<T>()?;
     export_type_to::<T, _>(&path)
 }
 
 /// Export `T` to the file specified by the `path` argument.
-pub(crate) fn export_type_to<T: TS + ?Sized, P: AsRef<Path>>(path: P) -> Result<(), ExportError> {
+pub(crate) fn export_type_to<T: TS + ?Sized + 'static, P: AsRef<Path>>(
+    path: P,
+) -> Result<(), ExportError> {
     #[allow(unused_mut)]
     let mut buffer = export_type_to_string::<T>()?;
 
@@ -54,7 +56,7 @@ pub(crate) fn export_type_to<T: TS + ?Sized, P: AsRef<Path>>(path: P) -> Result<
 }
 
 /// Returns the generated defintion for `T`.
-pub(crate) fn export_type_to_string<T: TS + ?Sized>() -> Result<String, ExportError> {
+pub(crate) fn export_type_to_string<T: TS + ?Sized + 'static>() -> Result<String, ExportError> {
     let mut buffer = String::with_capacity(1024);
     buffer.push_str(NOTE);
     generate_imports::<T>(&mut buffer)?;
@@ -77,7 +79,7 @@ fn generate_decl<T: TS + ?Sized>(out: &mut String) {
 }
 
 /// Push an import statement for all dependencies of `T`
-fn generate_imports<T: TS + ?Sized>(out: &mut String) -> Result<(), ExportError> {
+fn generate_imports<T: TS + ?Sized + 'static>(out: &mut String) -> Result<(), ExportError> {
     let path = Path::new(T::EXPORT_TO.ok_or(ExportError::CannotBeExported)?);
 
     let deps = T::dependencies();
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index bec2c36..72d52ff 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -235,7 +235,7 @@ mod export;
 ///
 /// - `#[ts(skip)]`:  
 ///   Skip this variant  
-pub trait TS: 'static {
+pub trait TS {
     const EXPORT_TO: Option<&'static str> = None;
 
     /// Declaration of this type, e.g. `interface User { user_id: number, ... }`.
@@ -266,7 +266,9 @@ pub trait TS: 'static {
 
     /// Information about types this type depends on.
     /// This is used for resolving imports when exporting to a file.
-    fn dependencies() -> Vec<Dependency>;
+    fn dependencies() -> Vec<Dependency>
+    where
+        Self: 'static;
 
     /// `true` if this is a transparent type, e.g tuples or a list.  
     /// This is used for resolving imports when using the `export!` macro.
@@ -279,19 +281,28 @@ pub trait TS: 'static {
     /// When a type is annotated with `#[ts(export)]`, it is exported automatically within a test.
     /// This function is only usefull if you need to export the type outside of the context of a
     /// test.
-    fn export() -> Result<(), ExportError> {
+    fn export() -> Result<(), ExportError>
+    where
+        Self: 'static,
+    {
         export::export_type::<Self>()
     }
 
     /// Manually export this type to a file with a file with the specified path. This
     /// function will ignore the `#[ts(export_to = "..)]` attribute.
-    fn export_to(path: impl AsRef<Path>) -> Result<(), ExportError> {
+    fn export_to(path: impl AsRef<Path>) -> Result<(), ExportError>
+    where
+        Self: 'static,
+    {
         export::export_type_to::<Self, _>(path)
     }
 
     /// Manually generate bindings for this type, returning a [`String`].  
     /// This function does not format the output, even if the `format` feature is enabled.
-    fn export_to_string() -> Result<String, ExportError> {
+    fn export_to_string() -> Result<String, ExportError>
+    where
+        Self: 'static,
+    {
         export::export_type_to_string::<Self>()
     }
 }
@@ -313,7 +324,7 @@ impl Dependency {
     /// Constructs a [`Dependency`] from the given type `T`.
     /// If `T` is not exportable (meaning `T::EXPORT_TO` is `None`), this function will return
     /// `None`
-    pub fn from_ty<T: TS>() -> Option<Self> {
+    pub fn from_ty<T: TS + 'static + ?Sized>() -> Option<Self> {
         let exported_to = T::EXPORT_TO?;
         Some(Dependency {
             type_id: TypeId::of::<T>(),
@@ -348,7 +359,10 @@ macro_rules! impl_tuples {
             fn inline() -> String {
                 format!("[{}]", vec![ $($i::inline()),* ].join(", "))
             }
-            fn dependencies() -> Vec<Dependency> {
+            fn dependencies() -> Vec<Dependency>
+            where
+                Self: 'static
+            {
                 [$( Dependency::from_ty::<$i>() ),*]
                 .into_iter()
                 .flatten()
@@ -375,7 +389,12 @@ macro_rules! impl_wrapper {
             }
             fn inline() -> String { T::inline() }
             fn inline_flattened() -> String { T::inline_flattened() }
-            fn dependencies() -> Vec<Dependency> { T::dependencies() }
+            fn dependencies() -> Vec<Dependency>
+            where
+                Self: 'static
+            {
+                T::dependencies()
+            }
             fn transparent() -> bool { T::transparent() }
         }
     };
@@ -389,7 +408,12 @@ macro_rules! impl_shadow {
             fn name_with_type_args(args: Vec<String>) -> String { <$s>::name_with_type_args(args) }
             fn inline() -> String { <$s>::inline() }
             fn inline_flattened() -> String { <$s>::inline_flattened() }
-            fn dependencies() -> Vec<$crate::Dependency> { <$s>::dependencies() }
+            fn dependencies() -> Vec<$crate::Dependency>
+            where
+                Self: 'static
+            {
+                <$s>::dependencies()
+            }
             fn transparent() -> bool { <$s>::transparent() }
         }
     };
@@ -414,7 +438,10 @@ impl<T: TS> TS for Option<T> {
         format!("{} | null", T::inline())
     }
 
-    fn dependencies() -> Vec<Dependency> {
+    fn dependencies() -> Vec<Dependency>
+    where
+        Self: 'static,
+    {
         [Dependency::from_ty::<T>()].into_iter().flatten().collect()
     }
 
@@ -442,7 +469,10 @@ impl<T: TS> TS for Vec<T> {
         format!("Array<{}>", T::inline())
     }
 
-    fn dependencies() -> Vec<Dependency> {
+    fn dependencies() -> Vec<Dependency>
+    where
+        Self: 'static,
+    {
         [Dependency::from_ty::<T>()].into_iter().flatten().collect()
     }
 
@@ -470,7 +500,10 @@ impl<K: TS, V: TS> TS for HashMap<K, V> {
         format!("Record<{}, {}>", K::inline(), V::inline())
     }
 
-    fn dependencies() -> Vec<Dependency> {
+    fn dependencies() -> Vec<Dependency>
+    where
+        Self: 'static,
+    {
         [Dependency::from_ty::<K>(), Dependency::from_ty::<V>()]
             .into_iter()
             .flatten()
@@ -497,7 +530,10 @@ impl<I: TS> TS for Range<I> {
         format!("{{ start: {}, end: {}, }}", &args[0], &args[0])
     }
 
-    fn dependencies() -> Vec<Dependency> {
+    fn dependencies() -> Vec<Dependency>
+    where
+        Self: 'static,
+    {
         [Dependency::from_ty::<I>()].into_iter().flatten().collect()
     }
 
@@ -521,7 +557,10 @@ impl<I: TS> TS for RangeInclusive<I> {
         format!("{{ start: {}, end: {}, }}", &args[0], &args[0])
     }
 
-    fn dependencies() -> Vec<Dependency> {
+    fn dependencies() -> Vec<Dependency>
+    where
+        Self: 'static,
+    {
         [Dependency::from_ty::<I>()].into_iter().flatten().collect()
     }
 
@@ -530,19 +569,20 @@ impl<I: TS> TS for RangeInclusive<I> {
     }
 }
 
+impl_shadow!(as T: impl<'a, T: TS + ?Sized> TS for &T);
 impl_shadow!(as Vec<T>: impl<T: TS> TS for HashSet<T>);
 impl_shadow!(as Vec<T>: impl<T: TS> TS for BTreeSet<T>);
 impl_shadow!(as HashMap<K, V>: impl<K: TS, V: TS> TS for BTreeMap<K, V>);
 impl_shadow!(as Vec<T>: impl<T: TS, const N: usize> TS for [T; N]);
 
-impl_wrapper!(impl<T: TS> TS for Box<T>);
-impl_wrapper!(impl<T: TS> TS for std::sync::Arc<T>);
-impl_wrapper!(impl<T: TS> TS for std::rc::Rc<T>);
-impl_wrapper!(impl<T: TS + ToOwned> TS for std::borrow::Cow<'static, T>);
+impl_wrapper!(impl<T: TS + ?Sized> TS for Box<T>);
+impl_wrapper!(impl<T: TS + ?Sized> TS for std::sync::Arc<T>);
+impl_wrapper!(impl<T: TS + ?Sized> TS for std::rc::Rc<T>);
+impl_wrapper!(impl<'a, T: TS + ToOwned + ?Sized> TS for std::borrow::Cow<'a, T>);
 impl_wrapper!(impl<T: TS> TS for std::cell::Cell<T>);
 impl_wrapper!(impl<T: TS> TS for std::cell::RefCell<T>);
 impl_wrapper!(impl<T: TS> TS for std::sync::Mutex<T>);
-impl_wrapper!(impl<T: TS> TS for std::sync::Weak<T>);
+impl_wrapper!(impl<T: TS + ?Sized> TS for std::sync::Weak<T>);
 impl_wrapper!(impl<T: TS> TS for std::marker::PhantomData<T>);
 
 impl_tuples!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10);
@@ -584,7 +624,7 @@ impl_primitives! {
     u64, i64, NonZeroU64, NonZeroI64,
     u128, i128, NonZeroU128, NonZeroI128 => "bigint",
     bool => "boolean",
-    char, Path, PathBuf, String, &'static str,
+    char, Path, PathBuf, String, str,
     Ipv4Addr, Ipv6Addr, IpAddr, SocketAddrV4, SocketAddrV6, SocketAddr => "string",
     () => "null"
 }


Test patch:
diff --git a/ts-rs/tests/lifetimes.rs b/ts-rs/tests/lifetimes.rs
new file mode 100644
index 0000000..75bdb64
--- /dev/null
+++ b/ts-rs/tests/lifetimes.rs
@@ -0,0 +1,12 @@
+use ts_rs::TS;
+
+#[test]
+fn contains_borrow() {
+    #[derive(TS)]
+    #[allow(dead_code)]
+    struct S<'a> {
+        s: &'a str,
+    }
+
+    assert_eq!(S::decl(), "interface S { s: string, }")
+}
diff --git a/ts-rs/tests/unsized.rs b/ts-rs/tests/unsized.rs
new file mode 100644
index 0000000..61c0a26
--- /dev/null
+++ b/ts-rs/tests/unsized.rs
@@ -0,0 +1,20 @@
+use std::{borrow::Cow, rc::Rc, sync::Arc};
+
+use ts_rs::TS;
+
+#[test]
+fn contains_str() {
+    #[derive(TS)]
+    #[allow(dead_code)]
+    struct S<'a> {
+        b: Box<str>,
+        c: Cow<'a, str>,
+        r: Rc<str>,
+        a: Arc<str>,
+    }
+
+    assert_eq!(
+        S::decl(),
+        "interface S { b: string, c: string, r: string, a: string, }"
+    )
+}