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

Issue:
Fields with `skip_deserializing` not getting exported?
Hello!
Thanks for the great crate, I find it very useful!

I'm just wondering if there's a reason why fields marked with `#[serde(skip_deserializing)]` are getting skipped? I saw in [the macro code](https://github.com/Aleph-Alpha/ts-rs/blob/b4ba8b81fd8833296e99285eae7608864c52e51e/macros/src/attr/field.rs#L66) that this is a deliberate decision, but I fail to see the reasoning behind it.

As exporting types are in essence serializing them, I would logically assume that the bindings would still include those fields.

Best regards!

Pull request:
Remove support for "skip_serializing", "skip_deserializing" and "skip_serializing_if"
We plan on removing "support" for these in the next major release.  
Currently, fields annotated with one of these serde annotations are skipped. However, this behavious is incorrect, depending on your usecase. 

Resolves #143


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..fce6d7a
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# master
+
+- Remove support for "skip_serializing", "skip_serializing_if" and "skip_deserializing".
+    - Initially supporting these by skipping a field was a mistake. If a user wishes to skip a field, they can still
+      annotate it with `#[ts(skip)]`
\ No newline at end of file
diff --git a/README.md b/README.md
index d1df8d5..158a86c 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ generate typescript interface/type declarations from rust types
 
 <div align="center">
 <!-- Github Actions -->
-<img src="https://img.shields.io/github/workflow/status/Aleph-Alpha/ts-rs/Test?style=flat-square" alt="actions status" />
+<img src="https://img.shields.io/github/actions/workflow/status/Aleph-Alpha/ts-rs/test.yml?branch=main" alt="actions status" />
 <a href="https://crates.io/crates/ts-rs">
 <img src="https://img.shields.io/crates/v/ts-rs.svg?style=flat-square"
 alt="Crates.io version" />
@@ -131,12 +131,12 @@ Supported serde attributes:
 - `content`
 - `untagged`
 - `skip`
-- `skip_serializing`
-- `skip_deserializing`
-- `skip_serializing_if = "Option::is_none"`
 - `flatten`
 - `default`
 
+Note: `skip_serializing` and `skip_deserializing` are ignored. If you wish to exclude a field
+from the generated type, but cannot use `#[serde(skip)]`, use `#[ts(skip)]` instead.
+
 When ts-rs encounters an unsupported serde attribute, a warning is emitted, unless the feature `no-serde-warnings` is enabled.
 
 ### contributing
diff --git a/example/src/lib.rs b/example/src/lib.rs
index dfdbd81..81e847a 100644
--- a/example/src/lib.rs
+++ b/example/src/lib.rs
@@ -102,6 +102,6 @@ enum InlineComplexEnum {
 #[serde(rename_all = "camelCase")]
 #[ts(export)]
 struct ComplexStruct {
-    #[serde(default, skip_serializing_if = "Option::is_none")]
+    #[serde(default)]
     pub string_tree: Option<Rc<BTreeSet<String>>>,
 }
diff --git a/macros/src/attr/field.rs b/macros/src/attr/field.rs
index 0c91b1c..aab124a 100644
--- a/macros/src/attr/field.rs
+++ b/macros/src/attr/field.rs
@@ -65,9 +65,6 @@ impl_parse! {
     SerdeFieldAttr(input, out) {
         "rename" => out.0.rename = Some(parse_assign_str(input)?),
         "skip" => out.0.skip = true,
-        "skip_serializing" => out.0.skip = true,
-        "skip_deserializing" => out.0.skip = true,
-        "skip_serializing_if" => out.0.optional = parse_assign_str(input)? == *"Option::is_none",
         "flatten" => out.0.flatten = true,
         // parse #[serde(default)] to not emit a warning
         "default" => {
diff --git a/macros/src/attr/variant.rs b/macros/src/attr/variant.rs
index 1f490db..74a3c28 100644
--- a/macros/src/attr/variant.rs
+++ b/macros/src/attr/variant.rs
@@ -60,7 +60,5 @@ impl_parse! {
         "rename" => out.0.rename = Some(parse_assign_str(input)?),
         "rename_all" => out.0.rename_all = Some(parse_assign_inflection(input)?),
         "skip" => out.0.skip = true,
-        "skip_serializing" => out.0.skip = true,
-        "skip_deserializing" => out.0.skip = true,
     }
 }
diff --git a/macros/src/types/named.rs b/macros/src/types/named.rs
index 8786124..1a3249b 100644
--- a/macros/src/types/named.rs
+++ b/macros/src/types/named.rs
@@ -45,7 +45,7 @@ pub(crate) fn named(
                 #fields,
             )
         },
-        decl: quote!(format!("interface {}{} {}", #name, #generic_args, Self::inline())),
+        decl: quote!(format!("type {}{} = {}", #name, #generic_args, Self::inline())),
         inline_flattened: Some(fields),
         name: name.to_owned(),
         dependencies,
diff --git a/ts-rs/src/lib.rs b/ts-rs/src/lib.rs
index c52eb95..66dd451 100644
--- a/ts-rs/src/lib.rs
+++ b/ts-rs/src/lib.rs
@@ -9,7 +9,7 @@
 //!
 //! <div align="center">
 //! <!-- Github Actions -->
-//! <img src="https://img.shields.io/github/workflow/status/Aleph-Alpha/ts-rs/Test?style=flat-square" alt="actions status" />
+//! <img src="https://img.shields.io/github/actions/workflow/status/Aleph-Alpha/ts-rs/test.yml?branch=main" alt="actions status" />
 //! <a href="https://crates.io/crates/ts-rs">
 //! <img src="https://img.shields.io/crates/v/ts-rs.svg?style=flat-square"
 //! alt="Crates.io version" />
@@ -129,12 +129,12 @@
 //! - `content`
 //! - `untagged`
 //! - `skip`
-//! - `skip_serializing`
-//! - `skip_deserializing`
-//! - `skip_serializing_if = "Option::is_none"`
 //! - `flatten`
 //! - `default`
 //!
+//! Note: `skip_serializing` and `skip_deserializing` are ignored. If you wish to exclude a field
+//! from the generated type, but cannot use `#[serde(skip)]`, use `#[ts(skip)]` instead.
+//!
 //! When ts-rs encounters an unsupported serde attribute, a warning is emitted, unless the feature `no-serde-warnings` is enabled.
 //!
 //! ## contributing
@@ -521,7 +521,7 @@ impl<T: TS> TS for Vec<T> {
     }
 }
 
-impl<K: TS, V: TS> TS for HashMap<K, V> {
+impl<K: TS, V: TS, H> TS for HashMap<K, V, H> {
     fn name() -> String {
         "Record".to_owned()
     }
@@ -610,7 +610,7 @@ 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, 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]);


Test patch:
diff --git a/ts-rs/tests/chrono.rs b/ts-rs/tests/chrono.rs
index cc52784..d9bc9ee 100644
--- a/ts-rs/tests/chrono.rs
+++ b/ts-rs/tests/chrono.rs
@@ -27,6 +27,6 @@ fn chrono() {
 
     assert_eq!(
         Chrono::decl(),
-        "interface Chrono { date: [string, string, string, string], time: string, date_time: [string, string, string, string], duration: string, month: string, weekday: string, }"
+        "type Chrono = { date: [string, string, string, string], time: string, date_time: [string, string, string, string], duration: string, month: string, weekday: string, }"
     )
 }
diff --git a/ts-rs/tests/export_manually.rs b/ts-rs/tests/export_manually.rs
index aaae5cf..33a7af1 100644
--- a/ts-rs/tests/export_manually.rs
+++ b/ts-rs/tests/export_manually.rs
@@ -27,16 +27,12 @@ fn export_manually() {
     let expected_content = if cfg!(feature = "format") {
         concat!(
             "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\n",
-            "export interface User {\n",
-            "  name: string;\n",
-            "  age: number;\n",
-            "  active: boolean;\n",
-            "}\n"
+            "export type User = { name: string; age: number; active: boolean };\n"
         )
     } else {
         concat!(
             "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n",
-            "\nexport interface User { name: string, age: number, active: boolean, }"
+            "\nexport type User = { name: string, age: number, active: boolean, }"
         )
     };
 
@@ -52,16 +48,12 @@ fn export_manually_dir() {
     let expected_content = if cfg!(feature = "format") {
         concat!(
             "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n\n",
-            "export interface UserDir {\n",
-            "  name: string;\n",
-            "  age: number;\n",
-            "  active: boolean;\n",
-            "}\n"
+            "export type UserDir = { name: string; age: number; active: boolean };\n"
         )
     } else {
         concat!(
             "// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.\n",
-            "\nexport interface UserDir { name: string, age: number, active: boolean, }"
+            "\nexport type UserDir = { name: string, age: number, active: boolean, }"
         )
     };
 
diff --git a/ts-rs/tests/generics.rs b/ts-rs/tests/generics.rs
index 5dad985..cd8cd68 100644
--- a/ts-rs/tests/generics.rs
+++ b/ts-rs/tests/generics.rs
@@ -60,27 +60,27 @@ declare! {
 fn test() {
     assert_eq!(
         TypeGroup::decl(),
-        "interface TypeGroup { foo: Array<Container>, }",
+        "type TypeGroup = { foo: Array<Container>, }",
     );
 
     assert_eq!(
         Generic::<()>::decl(),
-        "interface Generic<T> { value: T, values: Array<T>, }"
+        "type Generic<T> = { value: T, values: Array<T>, }"
     );
 
     assert_eq!(
         GenericAutoBound::<()>::decl(),
-        "interface GenericAutoBound<T> { value: T, values: Array<T>, }"
+        "type GenericAutoBound<T> = { value: T, values: Array<T>, }"
     );
 
     assert_eq!(
         GenericAutoBound2::<()>::decl(),
-        "interface GenericAutoBound2<T> { value: T, values: Array<T>, }"
+        "type GenericAutoBound2<T> = { value: T, values: Array<T>, }"
     );
 
     assert_eq!(
         Container::decl(),
-        "interface Container { foo: Generic<number>, bar: Array<Generic<number>>, baz: Record<string, Generic<string>>, }"
+        "type Container = { foo: Generic<number>, bar: Array<Generic<number>>, baz: Record<string, Generic<string>>, }"
     );
 }
 
@@ -142,7 +142,7 @@ fn generic_struct() {
 
     assert_eq!(
         Struct::<()>::decl(),
-        "interface 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: Array<T>, e: Array<[T, T]>, f: Array<T>, g: Array<Array<T>>, h: Array<Array<[T, T]>>, }"
     )
 }
 
@@ -164,10 +164,10 @@ fn inline() {
         t: Generic<String>,
     }
 
-    assert_eq!(Generic::<()>::decl(), "interface Generic<T> { t: T, }");
+    assert_eq!(Generic::<()>::decl(), "type Generic<T> = { t: T, }");
     assert_eq!(
         Container::decl(),
-        "interface Container { g: Generic<string>, gi: { t: string }, t: string, }"
+        "type Container = { g: Generic<string>, gi: { t: string }, t: string, }"
     );
 }
 
@@ -177,7 +177,7 @@ fn default() {
     struct A<T = String> {
         t: T,
     }
-    assert_eq!(A::<()>::decl(), "interface A<T = string> { t: T, }");
+    assert_eq!(A::<()>::decl(), "type A<T = string> = { t: T, }");
 
     #[derive(TS)]
     struct B<U = Option<A<i32>>> {
@@ -185,7 +185,7 @@ fn default() {
     }
     assert_eq!(
         B::<()>::decl(),
-        "interface B<U = A<number> | null> { u: U, }"
+        "type B<U = A<number> | null> = { u: U, }"
     );
     assert!(B::<()>::dependencies().iter().any(|dep| dep.ts_name == "A"));
 
@@ -200,7 +200,7 @@ fn default() {
         // #[ts(inline)]
         // xi2: X<i32>
     }
-    assert_eq!(Y::decl(), "interface Y { a1: A, a2: A<number>, }")
+    assert_eq!(Y::decl(), "type Y = { a1: A, a2: A<number>, }")
 }
 
 #[test]
@@ -209,7 +209,7 @@ fn trait_bounds() {
     struct A<T: ToString = i32> {
         t: T,
     }
-    assert_eq!(A::<i32>::decl(), "interface A<T = number> { t: T, }");
+    assert_eq!(A::<i32>::decl(), "type A<T = number> = { t: T, }");
 
     #[derive(TS)]
     struct B<T: ToString + Debug + Clone + 'static>(T);
@@ -232,5 +232,5 @@ fn trait_bounds() {
         t: [T; N],
     }
 
-    assert_eq!(D::<&str, 41>::decl(), "interface D<T> { t: Array<T>, }")
+    assert_eq!(D::<&str, 41>::decl(), "type D<T> = { t: Array<T>, }")
 }
diff --git a/ts-rs/tests/hashmap.rs b/ts-rs/tests/hashmap.rs
new file mode 100644
index 0000000..a588e9c
--- /dev/null
+++ b/ts-rs/tests/hashmap.rs
@@ -0,0 +1,37 @@
+use std::collections::{HashMap, HashSet};
+use ts_rs::TS;
+
+#[test]
+fn hashmap() {
+    #[derive(TS)]
+    #[allow(dead_code)]
+    struct Hashes {
+        map: HashMap<String, String>,
+        set: HashSet<String>,
+    }
+
+    assert_eq!(
+        Hashes::decl(),
+        "type Hashes = { map: Record<string, string>, set: Array<string>, }"
+    )
+}
+
+#[test]
+fn hashmap_with_custom_hasher() {
+    struct CustomHasher {}
+
+    type CustomHashMap<K, V> = HashMap<K, V, CustomHasher>;
+    type CustomHashSet<K> = HashSet<K, CustomHasher>;
+
+    #[derive(TS)]
+    #[allow(dead_code)]
+    struct Hashes {
+        map: CustomHashMap<String, String>,
+        set: CustomHashSet<String>,
+    }
+
+    assert_eq!(
+        Hashes::decl(),
+        "type Hashes = { map: Record<string, string>, set: Array<string>, }"
+    )
+}
diff --git a/ts-rs/tests/indexmap.rs b/ts-rs/tests/indexmap.rs
index 8b7b05e..7c8eb20 100644
--- a/ts-rs/tests/indexmap.rs
+++ b/ts-rs/tests/indexmap.rs
@@ -14,6 +14,6 @@ fn indexmap() {
 
     assert_eq!(
         Indexes::decl(),
-        "interface Indexes { map: Record<string, string>, set: Array<string>, }"
+        "type Indexes = { map: Record<string, string>, set: Array<string>, }"
     )
 }
diff --git a/ts-rs/tests/lifetimes.rs b/ts-rs/tests/lifetimes.rs
index 835610e..10abf37 100644
--- a/ts-rs/tests/lifetimes.rs
+++ b/ts-rs/tests/lifetimes.rs
@@ -8,7 +8,7 @@ fn contains_borrow() {
         s: &'a str,
     }
 
-    assert_eq!(S::decl(), "interface S { s: string, }")
+    assert_eq!(S::decl(), "type S = { s: string, }")
 }
 
 #[test]
@@ -29,6 +29,6 @@ fn contains_borrow_type_args() {
 
     assert_eq!(
         A::decl(),
-        "interface A { a: Array<number>, b: Array<B<number>>, c: Record<string, boolean>, }"
+        "type A = { a: Array<number>, b: Array<B<number>>, c: Record<string, boolean>, }"
     );
 }
diff --git a/ts-rs/tests/list.rs b/ts-rs/tests/list.rs
index 65aaa00..dfa3126 100644
--- a/ts-rs/tests/list.rs
+++ b/ts-rs/tests/list.rs
@@ -10,6 +10,6 @@ fn list() {
 
     assert_eq!(
         List::decl(),
-        "interface List { data: Array<number> | null, }"
+        "type List = { data: Array<number> | null, }"
     );
 }
diff --git a/ts-rs/tests/optional_field.rs b/ts-rs/tests/optional_field.rs
index 807e9da..4912584 100644
--- a/ts-rs/tests/optional_field.rs
+++ b/ts-rs/tests/optional_field.rs
@@ -7,14 +7,10 @@ use ts_rs::TS;
 struct Optional {
     #[ts(optional)]
     a: Option<i32>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    b: Option<String>,
+    b: Option<String>
 }
 
 #[test]
 fn test() {
-    #[cfg(not(feature = "serde-compat"))]
     assert_eq!(Optional::inline(), "{ a?: number, b: string | null, }");
-    #[cfg(feature = "serde-compat")]
-    assert_eq!(Optional::inline(), "{ a?: number, b?: string, }")
 }
diff --git a/ts-rs/tests/ranges.rs b/ts-rs/tests/ranges.rs
index ac99070..10cdcd6 100644
--- a/ts-rs/tests/ranges.rs
+++ b/ts-rs/tests/ranges.rs
@@ -19,7 +19,7 @@ struct RangeTest {
 fn range() {
     assert_eq!(
         RangeTest::decl(),
-        "interface RangeTest { a: { start: number, end: number, }, b: { start: string, end: string, }, c: { start: { start: number, end: number, }, end: { start: number, end: number, }, }, d: { start: number, end: number, }, e: { start: Inner, end: Inner, }, }"
+        "type RangeTest = { a: { start: number, end: number, }, b: { start: string, end: string, }, c: { start: { start: number, end: number, }, end: { start: number, end: number, }, }, d: { start: number, end: number, }, e: { start: Inner, end: Inner, }, }"
     );
     assert_eq!(
         RangeTest::dependencies(),
diff --git a/ts-rs/tests/raw_idents.rs b/ts-rs/tests/raw_idents.rs
index 51ee787..9fae879 100644
--- a/ts-rs/tests/raw_idents.rs
+++ b/ts-rs/tests/raw_idents.rs
@@ -15,6 +15,6 @@ fn raw_idents() {
     let out = <r#enum as TS>::decl();
     assert_eq!(
         out,
-        "interface enum { type: number, use: number, struct: number, let: number, enum: number, }"
+        "type enum = { type: number, use: number, struct: number, let: number, enum: number, }"
     );
 }
diff --git a/ts-rs/tests/serde-skip-with-default.rs b/ts-rs/tests/serde-skip-with-default.rs
index 6287f36..5f970af 100644
--- a/ts-rs/tests/serde-skip-with-default.rs
+++ b/ts-rs/tests/serde-skip-with-default.rs
@@ -22,6 +22,6 @@ pub struct Foobar {
 fn serde_skip_with_default() {
     assert_eq!(
         Foobar::decl(),
-        "interface Foobar { something_else: number, }"
+        "type Foobar = { something_else: number, }"
     );
 }
diff --git a/ts-rs/tests/union_with_data.rs b/ts-rs/tests/union_with_data.rs
index 2c69739..b35f368 100644
--- a/ts-rs/tests/union_with_data.rs
+++ b/ts-rs/tests/union_with_data.rs
@@ -24,10 +24,10 @@ enum SimpleEnum {
 
 #[test]
 fn test_stateful_enum() {
-    assert_eq!(Bar::decl(), r#"interface Bar { field: number, }"#);
+    assert_eq!(Bar::decl(), r#"type Bar = { field: number, }"#);
     assert_eq!(Bar::dependencies(), vec![]);
 
-    assert_eq!(Foo::decl(), r#"interface Foo { bar: Bar, }"#);
+    assert_eq!(Foo::decl(), r#"type Foo = { bar: Bar, }"#);
     assert_eq!(
         Foo::dependencies(),
         vec![Dependency::from_ty::<Bar>().unwrap()]
diff --git a/ts-rs/tests/unsized.rs b/ts-rs/tests/unsized.rs
index 61c0a26..bfddf75 100644
--- a/ts-rs/tests/unsized.rs
+++ b/ts-rs/tests/unsized.rs
@@ -15,6 +15,6 @@ fn contains_str() {
 
     assert_eq!(
         S::decl(),
-        "interface S { b: string, c: string, r: string, a: string, }"
+        "type S = { b: string, c: string, r: string, a: string, }"
     )
 }