Repository: Aleph-Alpha/ts-rs (rust)

Issue:
Allow emitting arrays as TS tuples
```rust
pub(crate) struct Scene {
    color: [f32; 4],
}
```
currently becomes an array of uncertain length,
```typescript
export interface Scene { 
  color: Array<number>;
}
```

It would be useful to be able to mark this e.g.

```rust
pub(crate) struct Scene {
    #[ts(tuple)]
    color: [f32; 4],
}
```
to emit it as
```typescript
export interface Scene { 
  color: [number, number, number, number];
}
```

I tried my hand at implementing this myself, but got lost in macros...

Pull request description:
Convert fixed size Rust array to fixed size TS tuple
Fixes #137

Patch:
diff --git a/macros/src/types/generics.rs b/macros/src/types/generics.rs
index 5bf26ff..11993a8 100644
--- a/macros/src/types/generics.rs
+++ b/macros/src/types/generics.rs
@@ -1,7 +1,7 @@
 use proc_macro2::TokenStream;
 use quote::{format_ident, quote};
 use syn::{
-    GenericArgument, GenericParam, Generics, ItemStruct, PathArguments, Type, TypeArray, TypeGroup,
+    GenericArgument, GenericParam, Generics, ItemStruct, PathArguments, Type, TypeGroup,
     TypeReference, TypeSlice, TypeTuple,
 };
 
@@ -79,9 +79,15 @@ pub fn format_type(ty: &Type, dependencies: &mut Dependencies, generics: &Generi
 
     // special treatment for arrays and tuples
     match ty {
-        // The field is an array (`[T; n]`) or a slice (`[T]`) so it technically doesn't have a
+        // Arrays have their own implementation that needs to be handle separetly
+        // be cause the T in `[T; N]` is technically not a generic
+        Type::Array(type_array) => {
+            let formatted = format_type(&type_array.elem, dependencies, generics);
+            return quote!(<#type_array>::name_with_type_args(vec![#formatted]))
+        }
+        // The field is a slice (`[T]`) so it technically doesn't have a
         // generic argument. Therefore, we handle it explicitly here like a `Vec<T>`
-        Type::Array(TypeArray { ref elem, .. }) | Type::Slice(TypeSlice { ref elem, .. }) => {
+        Type::Slice(TypeSlice { ref elem, .. }) => {
             let inner_ty = elem;
             let vec_ty = syn::parse2::<Type>(quote!(Vec::<#inner_ty>)).unwrap();
             return format_type(&vec_ty, dependencies, generics);
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index a7d4a89..58c1939 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -498,18 +498,60 @@ impl<T: TS> TS for Vec<T> {
         "Array".to_owned()
     }
 
+    fn inline() -> String {
+        format!("Array<{}>", T::inline())
+    }
+
+    fn dependencies() -> Vec<Dependency>
+    where
+        Self: 'static,
+    {
+        [Dependency::from_ty::<T>()].into_iter().flatten().collect()
+    }
+
+    fn transparent() -> bool {
+        true
+    }
+}
+
+// Arrays longer than this limit will be emmited as Array<T>
+const ARRAY_TUPLE_LIMIT: usize = 128;
+impl<T: TS, const N: usize> TS for [T; N] {
+    fn name() -> String {
+        if N > ARRAY_TUPLE_LIMIT {
+            return Vec::<T>::name()
+        }
+
+        "[]".to_owned()
+    }
+
     fn name_with_type_args(args: Vec<String>) -> String {
+        if N > ARRAY_TUPLE_LIMIT {
+            return Vec::<T>::name_with_type_args(args);
+        }
+
         assert_eq!(
             args.len(),
             1,
-            "called Vec::name_with_type_args with {} args",
+            "called [T; N]::name_with_type_args with {} args",
             args.len()
         );
-        format!("Array<{}>", args[0])
+
+        format!(
+            "[{}]",
+            (0..N).map(|_| args[0].clone()).collect::<Box<[_]>>().join(", ")
+        )
     }
 
     fn inline() -> String {
-        format!("Array<{}>", T::inline())
+        if N > ARRAY_TUPLE_LIMIT {
+            return Vec::<T>::inline();
+        }
+
+        format!(
+            "[{}]",
+            (0..N).map(|_| T::inline()).collect::<Box<[_]>>().join(", ")
+        )
     }
 
     fn dependencies() -> Vec<Dependency>
@@ -616,7 +658,6 @@ impl_shadow!(as T: impl<'a, T: TS + ?Sized> TS for &T);
 impl_shadow!(as Vec<T>: impl<T: TS, H> TS for HashSet<T, H>);
 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_shadow!(as Vec<T>: impl<T: TS> TS for [T]);
 
 impl_wrapper!(impl<T: TS + ?Sized> TS for Box<T>);


Test patch:
diff --git a/ts-rs/tests/arrays.rs b/ts-rs/tests/arrays.rs
index fe37a33..f974b38 100644
--- a/ts-rs/tests/arrays.rs
+++ b/ts-rs/tests/arrays.rs
@@ -2,7 +2,7 @@ use ts_rs::TS;
 
 #[test]
 fn free() {
-    assert_eq!(<[String; 10]>::inline(), "Array<string>")
+    assert_eq!(<[String; 4]>::inline(), "[string, string, string, string]")
 }
 
 #[test]
@@ -10,16 +10,16 @@ fn interface() {
     #[derive(TS)]
     struct Interface {
         #[allow(dead_code)]
-        a: [i32; 10],
+        a: [i32; 4],
     }
 
-    assert_eq!(Interface::inline(), "{ a: Array<number>, }")
+    assert_eq!(Interface::inline(), "{ a: [number, number, number, number], }")
 }
 
 #[test]
 fn newtype() {
     #[derive(TS)]
-    struct Newtype(#[allow(dead_code)] [i32; 10]);
+    struct Newtype(#[allow(dead_code)] [i32; 4]);
 
-    assert_eq!(Newtype::inline(), "Array<number>")
+    assert_eq!(Newtype::inline(), "[number, number, number, number]")
 }
diff --git a/ts-rs/tests/generic_fields.rs b/ts-rs/tests/generic_fields.rs
index a6f362d..e2100fa 100644
--- a/ts-rs/tests/generic_fields.rs
+++ b/ts-rs/tests/generic_fields.rs
@@ -40,7 +40,7 @@ fn named() {
     }
     assert_eq!(
         Struct::inline(),
-        "{ a: Array<string>, b: [Array<string>, Array<string>], c: Array<Array<string>>, }"
+        "{ a: Array<string>, b: [Array<string>, Array<string>], c: [Array<string>, Array<string>, Array<string>], }"
     );
 }
 
@@ -52,7 +52,7 @@ fn named_nested() {
         b: (Vec<Vec<String>>, Vec<Vec<String>>),
         c: [Vec<Vec<String>>; 3],
     }
-    assert_eq!(Struct::inline(), "{ a: Array<Array<string>>, b: [Array<Array<string>>, Array<Array<string>>], c: Array<Array<Array<string>>>, }");
+    assert_eq!(Struct::inline(), "{ a: Array<Array<string>>, b: [Array<Array<string>>, Array<Array<string>>], c: [Array<Array<string>>, Array<Array<string>>, Array<Array<string>>], }");
 }
 
 #[test]
@@ -61,7 +61,7 @@ fn tuple() {
     struct Tuple(Vec<i32>, (Vec<i32>, Vec<i32>), [Vec<i32>; 3]);
     assert_eq!(
         Tuple::inline(),
-        "[Array<number>, [Array<number>, Array<number>], Array<Array<number>>]"
+        "[Array<number>, [Array<number>, Array<number>], [Array<number>, Array<number>, Array<number>]]"
     );
 }
 
@@ -75,6 +75,6 @@ fn tuple_nested() {
     );
     assert_eq!(
         Tuple::inline(),
-        "[Array<Array<number>>, [Array<Array<number>>, Array<Array<number>>], Array<Array<Array<number>>>]"
+        "[Array<Array<number>>, [Array<Array<number>>, Array<Array<number>>], [Array<Array<number>>, Array<Array<number>>, Array<Array<number>>]]"
     );
 }
diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs
index 703868e..1503c4a 100644
--- a/ts-rs/tests/generics.rs
+++ b/ts-rs/tests/generics.rs
@@ -1,4 +1,4 @@
-#![allow(clippy::box_collection)]
+#![allow(clippy::box_collection, clippy::enum_variant_names, dead_code)]
 #![allow(dead_code)]
 
 use std::{
@@ -142,7 +142,7 @@ fn generic_struct() {
 
     assert_eq!(
         Struct::<()>::decl(),
-        "type Struct<T> = { a: T, b: [T, T], c: [T, [T, T]], d: Array<T>, e: Array<[T, T]>, f: Array<T>, g: Array<Array<T>>, h: Array<Array<[T, T]>>, }"
+        "type Struct<T> = { a: T, b: [T, T], c: [T, [T, T]], d: [T, T, T], e: [[T, T], [T, T], [T, T]], f: Array<T>, g: Array<Array<T>>, h: Array<[[T, T], [T, T], [T, T]]>, }"
     )
 }
 
@@ -278,5 +278,6 @@ fn trait_bounds() {
         t: [T; N],
     }
 
-    assert_eq!(D::<&str, 41>::decl(), "type D<T> = { t: Array<T>, }")
+    let ty = format!("type D<T> = {{ t: [{}], }}", "T, ".repeat(41).trim_end_matches(", "));
+    assert_eq!(D::<&str, 41>::decl(), ty)
 }