Skip to main content

facet_typescript/
lib.rs

1//! Generate TypeScript type definitions from facet type metadata.
2//!
3//! This crate uses facet's reflection capabilities to generate TypeScript
4//! interfaces and types from any type that implements `Facet`.
5//!
6//! # Example
7//!
8//! ```
9//! use facet::Facet;
10//! use facet_typescript::to_typescript;
11//!
12//! #[derive(Facet)]
13//! struct User {
14//!     name: String,
15//!     age: u32,
16//!     email: Option<String>,
17//! }
18//!
19//! let ts = to_typescript::<User>();
20//! assert!(ts.contains("export interface User"));
21//! ```
22
23extern crate alloc;
24
25use alloc::collections::BTreeSet;
26use alloc::string::String;
27use alloc::vec::Vec;
28use core::fmt::Write;
29
30use facet_core::{Def, Facet, Field, Shape, StructKind, Type, UserType};
31
32/// Generate TypeScript definitions for a single type.
33///
34/// Returns a string containing the TypeScript interface or type declaration.
35pub fn to_typescript<T: Facet<'static>>() -> String {
36    let mut generator = TypeScriptGenerator::new();
37    generator.add_shape(T::SHAPE);
38    generator.finish()
39}
40
41/// Generator for TypeScript type definitions.
42///
43/// Use this when you need to generate multiple related types.
44pub struct TypeScriptGenerator {
45    output: String,
46    /// Types already generated (by type identifier)
47    generated: BTreeSet<&'static str>,
48    /// Types queued for generation
49    queue: Vec<&'static Shape>,
50    /// Indentation level
51    indent: usize,
52}
53
54impl Default for TypeScriptGenerator {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl TypeScriptGenerator {
61    /// Create a new TypeScript generator.
62    pub const fn new() -> Self {
63        Self {
64            output: String::new(),
65            generated: BTreeSet::new(),
66            queue: Vec::new(),
67            indent: 0,
68        }
69    }
70
71    /// Add a type to generate.
72    pub fn add_type<T: Facet<'static>>(&mut self) {
73        self.add_shape(T::SHAPE);
74    }
75
76    /// Add a shape to generate.
77    pub fn add_shape(&mut self, shape: &'static Shape) {
78        if !self.generated.contains(shape.type_identifier) {
79            self.queue.push(shape);
80        }
81    }
82
83    /// Finish generation and return the TypeScript code.
84    pub fn finish(mut self) -> String {
85        // Process queue until empty
86        while let Some(shape) = self.queue.pop() {
87            if self.generated.contains(shape.type_identifier) {
88                continue;
89            }
90            self.generated.insert(shape.type_identifier);
91            self.generate_shape(shape);
92        }
93        self.output
94    }
95
96    fn write_indent(&mut self) {
97        for _ in 0..self.indent {
98            self.output.push_str("  ");
99        }
100    }
101
102    #[inline]
103    fn shape_key(shape: &'static Shape) -> &'static str {
104        shape.type_identifier
105    }
106
107    /// Unwrap through options, pointers, transparent wrappers, and proxies to get the effective shape.
108    ///
109    /// Returns the unwrapped shape along with a flag indicating whether an `Option` was encountered.
110    fn unwrap_to_inner_shape(shape: &'static Shape) -> (&'static Shape, bool) {
111        // Unwrap Option<T> first so we can mark fields as optional.
112        if let Def::Option(opt) = &shape.def {
113            let (inner, _) = Self::unwrap_to_inner_shape(opt.t);
114            return (inner, true);
115        }
116        // Unwrap pointers (Arc, Box, etc.)
117        if let Def::Pointer(ptr) = &shape.def
118            && let Some(pointee) = ptr.pointee
119        {
120            return Self::unwrap_to_inner_shape(pointee);
121        }
122        // Unwrap transparent wrappers
123        if let Some(inner) = shape.inner {
124            let (inner_shape, is_optional) = Self::unwrap_to_inner_shape(inner);
125            return (inner_shape, is_optional);
126        }
127        // Handle proxy types - use the proxy's shape for serialization
128        if let Some(proxy_def) = shape.proxy {
129            return Self::unwrap_to_inner_shape(proxy_def.shape);
130        }
131        (shape, false)
132    }
133
134    /// Format a field for inline object types (e.g., in enum variants).
135    /// Returns a string like `"fieldName: Type"` or `"fieldName?: Type"` for Option fields or fields with defaults.
136    fn format_inline_field(&mut self, field: &Field, force_optional: bool) -> String {
137        let field_name = field.effective_name();
138        let field_shape = field.shape.get();
139        let has_default = field.default.is_some();
140
141        if let Def::Option(opt) = &field_shape.def {
142            let inner_type = self.type_for_shape(opt.t);
143            format!("{}?: {}", field_name, inner_type)
144        } else if force_optional || has_default {
145            let field_type = self.type_for_shape(field_shape);
146            format!("{}?: {}", field_name, field_type)
147        } else {
148            let field_type = self.type_for_shape(field_shape);
149            format!("{}: {}", field_name, field_type)
150        }
151    }
152
153    /// Collect inline field strings for a struct's fields, handling skip and flatten.
154    fn collect_inline_fields(
155        &mut self,
156        fields: &'static [Field],
157        force_optional: bool,
158    ) -> Vec<String> {
159        let mut flatten_stack: Vec<&'static str> = Vec::new();
160        self.collect_inline_fields_guarded(fields, force_optional, &mut flatten_stack)
161    }
162
163    fn collect_inline_fields_guarded(
164        &mut self,
165        fields: &'static [Field],
166        force_optional: bool,
167        flatten_stack: &mut Vec<&'static str>,
168    ) -> Vec<String> {
169        let mut result = Vec::new();
170        for field in fields {
171            if field.should_skip_serializing_unconditional() {
172                continue;
173            }
174            if field.is_flattened() {
175                let (inner_shape, parent_is_optional) =
176                    Self::unwrap_to_inner_shape(field.shape.get());
177                if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
178                    let inner_key = Self::shape_key(inner_shape);
179                    if flatten_stack.contains(&inner_key) {
180                        continue;
181                    }
182                    flatten_stack.push(inner_key);
183                    result.extend(self.collect_inline_fields_guarded(
184                        st.fields,
185                        force_optional || parent_is_optional,
186                        flatten_stack,
187                    ));
188                    flatten_stack.pop();
189                    continue;
190                }
191            }
192            result.push(self.format_inline_field(field, force_optional));
193        }
194        result
195    }
196
197    /// Check if a struct has any fields that will be serialized.
198    /// This accounts for skipped fields and flattened structs.
199    fn has_serializable_fields(
200        field_owner_shape: &'static Shape,
201        fields: &'static [Field],
202    ) -> bool {
203        let mut flatten_stack: Vec<&'static str> = Vec::new();
204        flatten_stack.push(Self::shape_key(field_owner_shape));
205        Self::has_serializable_fields_guarded(fields, &mut flatten_stack)
206    }
207
208    fn has_serializable_fields_guarded(
209        fields: &'static [Field],
210        flatten_stack: &mut Vec<&'static str>,
211    ) -> bool {
212        for field in fields {
213            if field.should_skip_serializing_unconditional() {
214                continue;
215            }
216            if field.is_flattened() {
217                let (inner_shape, _) = Self::unwrap_to_inner_shape(field.shape.get());
218                if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
219                    let inner_key = Self::shape_key(inner_shape);
220                    if flatten_stack.contains(&inner_key) {
221                        continue;
222                    }
223                    flatten_stack.push(inner_key);
224                    let has_fields =
225                        Self::has_serializable_fields_guarded(st.fields, flatten_stack);
226                    flatten_stack.pop();
227                    if has_fields {
228                        return true;
229                    }
230                    continue;
231                }
232            }
233            // Found a field that will be serialized
234            return true;
235        }
236        false
237    }
238
239    /// Write struct fields to output, handling skip and flatten recursively.
240    fn write_struct_fields_for_shape(
241        &mut self,
242        field_owner_shape: &'static Shape,
243        fields: &'static [Field],
244    ) {
245        let mut flatten_stack: Vec<&'static str> = Vec::new();
246        flatten_stack.push(Self::shape_key(field_owner_shape));
247        self.write_struct_fields_guarded(fields, false, &mut flatten_stack);
248    }
249
250    fn write_struct_fields_guarded(
251        &mut self,
252        fields: &'static [Field],
253        force_optional: bool,
254        flatten_stack: &mut Vec<&'static str>,
255    ) {
256        for field in fields {
257            if field.should_skip_serializing_unconditional() {
258                continue;
259            }
260            if field.is_flattened() {
261                let (inner_shape, parent_is_optional) =
262                    Self::unwrap_to_inner_shape(field.shape.get());
263                if let Type::User(UserType::Struct(st)) = &inner_shape.ty {
264                    let inner_key = Self::shape_key(inner_shape);
265                    if flatten_stack.contains(&inner_key) {
266                        continue;
267                    }
268                    flatten_stack.push(inner_key);
269                    self.write_struct_fields_guarded(
270                        st.fields,
271                        force_optional || parent_is_optional,
272                        flatten_stack,
273                    );
274                    flatten_stack.pop();
275                    continue;
276                }
277            }
278            self.write_field(field, force_optional);
279        }
280    }
281
282    /// Write a single field to the output.
283    fn write_field(&mut self, field: &Field, force_optional: bool) {
284        // Generate doc comment for field
285        if let [line] = field.doc {
286            // Short doc: collapse onto a single line, e.g. `/** Okay */`.
287            self.write_indent();
288            self.output.push_str("/**");
289            self.output.push_str(line);
290            self.output.push_str(" */\n");
291        } else if !field.doc.is_empty() {
292            self.write_indent();
293            self.output.push_str("/**\n");
294            for line in field.doc {
295                self.write_indent();
296                self.output.push_str(" *");
297                self.output.push_str(line);
298                self.output.push('\n');
299            }
300            self.write_indent();
301            self.output.push_str(" */\n");
302        }
303
304        let field_name = field.effective_name();
305        let field_shape = field.shape.get();
306
307        self.write_indent();
308
309        // Use optional marker for Option fields, fields with defaults, or when explicitly forced (flattened Option parents).
310        let has_default = field.default.is_some();
311
312        if let Def::Option(opt) = &field_shape.def {
313            let inner_type = self.type_for_shape(opt.t);
314            writeln!(self.output, "{}?: {};", field_name, inner_type).unwrap();
315        } else if force_optional || has_default {
316            let field_type = self.type_for_shape(field_shape);
317            writeln!(self.output, "{}?: {};", field_name, field_type).unwrap();
318        } else {
319            let field_type = self.type_for_shape(field_shape);
320            writeln!(self.output, "{}: {};", field_name, field_type).unwrap();
321        }
322    }
323
324    fn generate_shape(&mut self, shape: &'static Shape) {
325        // Handle transparent wrappers - generate the inner type instead
326        if let Some(inner) = shape.inner {
327            self.add_shape(inner);
328            // Generate a type alias
329            let inner_type = self.type_for_shape(inner);
330            writeln!(
331                self.output,
332                "export type {} = {};",
333                shape.type_identifier, inner_type
334            )
335            .unwrap();
336            self.output.push('\n');
337            return;
338        }
339
340        // Generate doc comment if present (before proxy handling so proxied types keep their docs)
341        if let [line] = shape.doc {
342            // Short doc: collapse onto a single line, e.g. `/** Okay */`.
343            self.output.push_str("/**");
344            self.output.push_str(line);
345            self.output.push_str(" */\n");
346        } else if !shape.doc.is_empty() {
347            self.output.push_str("/**\n");
348            for line in shape.doc {
349                self.output.push_str(" *");
350                self.output.push_str(line);
351                self.output.push('\n');
352            }
353            self.output.push_str(" */\n");
354        }
355
356        // Handle proxy types - use the proxy's shape for generation
357        // but keep the original type name
358        if let Some(proxy_def) = shape.proxy {
359            let proxy_shape = proxy_def.shape;
360            match &proxy_shape.ty {
361                Type::User(UserType::Struct(st)) => {
362                    self.generate_struct(shape, proxy_shape, st.fields, st.kind);
363                    return;
364                }
365                Type::User(UserType::Enum(en)) => {
366                    self.generate_enum(shape, en);
367                    return;
368                }
369                _ => {
370                    // For non-struct/enum proxies (scalars, tuples, collections, etc.),
371                    // generate a type alias to the proxy's type
372                    let proxy_type = self.type_for_shape(proxy_shape);
373                    writeln!(
374                        self.output,
375                        "export type {} = {};",
376                        shape.type_identifier, proxy_type
377                    )
378                    .unwrap();
379                    self.output.push('\n');
380                    return;
381                }
382            }
383        }
384
385        match &shape.ty {
386            Type::User(UserType::Struct(st)) => {
387                self.generate_struct(shape, shape, st.fields, st.kind);
388            }
389            Type::User(UserType::Enum(en)) => {
390                self.generate_enum(shape, en);
391            }
392            _ => {
393                // For other types, generate a type alias
394                let type_str = self.type_for_shape(shape);
395                writeln!(
396                    self.output,
397                    "export type {} = {};",
398                    shape.type_identifier, type_str
399                )
400                .unwrap();
401                self.output.push('\n');
402            }
403        }
404    }
405
406    fn generate_struct(
407        &mut self,
408        exported_shape: &'static Shape,
409        field_owner_shape: &'static Shape,
410        fields: &'static [Field],
411        kind: StructKind,
412    ) {
413        match kind {
414            StructKind::Unit => {
415                // Unit struct as null
416                writeln!(
417                    self.output,
418                    "export type {} = null;",
419                    exported_shape.type_identifier
420                )
421                .unwrap();
422            }
423            StructKind::TupleStruct | StructKind::Tuple => {
424                // Tuple as array type
425                let types: Vec<String> = fields
426                    .iter()
427                    .map(|f| self.type_for_shape(f.shape.get()))
428                    .collect();
429                writeln!(
430                    self.output,
431                    "export type {} = [{}];",
432                    exported_shape.type_identifier,
433                    types.join(", ")
434                )
435                .unwrap();
436            }
437            StructKind::Struct => {
438                // Empty structs should use `object` type to prevent accepting primitives
439                if !Self::has_serializable_fields(field_owner_shape, fields) {
440                    writeln!(
441                        self.output,
442                        "export type {} = object;",
443                        exported_shape.type_identifier
444                    )
445                    .unwrap();
446                } else {
447                    writeln!(
448                        self.output,
449                        "export interface {} {{",
450                        exported_shape.type_identifier
451                    )
452                    .unwrap();
453                    self.indent += 1;
454
455                    self.write_struct_fields_for_shape(field_owner_shape, fields);
456
457                    self.indent -= 1;
458                    self.output.push_str("}\n");
459                }
460            }
461        }
462        self.output.push('\n');
463    }
464
465    fn generate_enum(&mut self, shape: &'static Shape, enum_type: &facet_core::EnumType) {
466        // Check if all variants are unit variants (simple string union)
467        let all_unit = enum_type
468            .variants
469            .iter()
470            .all(|v| matches!(v.data.kind, StructKind::Unit));
471
472        // Check if the enum is untagged
473        let is_untagged = shape.is_untagged();
474
475        if let Some(tag_key) = shape.tag {
476            // Internally tagged enum: each variant is an object with the tag field
477            let mut variant_types = Vec::new();
478
479            for variant in enum_type.variants {
480                let variant_name = variant.effective_name();
481                match variant.data.kind {
482                    StructKind::Unit => {
483                        variant_types.push(format!("{{ {}: \"{}\" }}", tag_key, variant_name));
484                    }
485                    StructKind::TupleStruct if variant.data.fields.len() == 1 => {
486                        // Newtype variant: tag merged into inner type via intersection
487                        let inner = self.type_for_shape(variant.data.fields[0].shape.get());
488                        variant_types.push(format!(
489                            "{{ {}: \"{}\" }} & {}",
490                            tag_key, variant_name, inner
491                        ));
492                    }
493                    StructKind::TupleStruct => {
494                        // Multi-element tuple variant: tag + tuple payload
495                        let types: Vec<String> = variant
496                            .data
497                            .fields
498                            .iter()
499                            .map(|f| self.type_for_shape(f.shape.get()))
500                            .collect();
501                        variant_types.push(format!(
502                            "{{ {}: \"{}\"; _: [{}] }}",
503                            tag_key,
504                            variant_name,
505                            types.join(", ")
506                        ));
507                    }
508                    _ => {
509                        // Struct variant: { tag: "VariantName"; field1: T1; field2: T2 }
510                        let field_types = self.collect_inline_fields(variant.data.fields, false);
511                        variant_types.push(format!(
512                            "{{ {}: \"{}\"; {} }}",
513                            tag_key,
514                            variant_name,
515                            field_types.join("; ")
516                        ));
517                    }
518                }
519            }
520
521            writeln!(
522                self.output,
523                "export type {} =\n  | {};",
524                shape.type_identifier,
525                variant_types.join("\n  | ")
526            )
527            .unwrap();
528        } else if is_untagged {
529            // Untagged enum: simple union of variant types
530            let mut variant_types = Vec::new();
531
532            for variant in enum_type.variants {
533                match variant.data.kind {
534                    StructKind::Unit => {
535                        // Unit variant in untagged enum - serializes as variant name string
536                        let variant_name = variant.effective_name();
537                        variant_types.push(format!("\"{}\"", variant_name));
538                    }
539                    StructKind::TupleStruct if variant.data.fields.len() == 1 => {
540                        // Newtype variant: just the inner type
541                        let inner = self.type_for_shape(variant.data.fields[0].shape.get());
542                        variant_types.push(inner);
543                    }
544                    StructKind::TupleStruct => {
545                        // Multi-element tuple variant: [T1, T2, ...]
546                        let types: Vec<String> = variant
547                            .data
548                            .fields
549                            .iter()
550                            .map(|f| self.type_for_shape(f.shape.get()))
551                            .collect();
552                        variant_types.push(format!("[{}]", types.join(", ")));
553                    }
554                    _ => {
555                        // Struct variant: inline object type
556                        let field_types = self.collect_inline_fields(variant.data.fields, false);
557                        variant_types.push(format!("{{ {} }}", field_types.join("; ")));
558                    }
559                }
560            }
561
562            writeln!(
563                self.output,
564                "export type {} = {};",
565                shape.type_identifier,
566                variant_types.join(" | ")
567            )
568            .unwrap();
569        } else if all_unit {
570            // Simple string literal union
571            let variants: Vec<String> = enum_type
572                .variants
573                .iter()
574                .map(|v| format!("\"{}\"", v.effective_name()))
575                .collect();
576            writeln!(
577                self.output,
578                "export type {} = {};",
579                shape.type_identifier,
580                variants.join(" | ")
581            )
582            .unwrap();
583        } else {
584            // Discriminated union
585            // Generate each variant as a separate interface, then union them
586            let mut variant_types = Vec::new();
587
588            for variant in enum_type.variants {
589                let variant_name = variant.effective_name();
590                match variant.data.kind {
591                    StructKind::Unit => {
592                        // Unit variant serializes as bare string, even in tagged enums.
593                        variant_types.push(format!("\"{}\"", variant_name));
594                    }
595                    StructKind::TupleStruct if variant.data.fields.len() == 1 => {
596                        // Newtype variant: { VariantName: InnerType }
597                        let inner = self.type_for_shape(variant.data.fields[0].shape.get());
598                        variant_types.push(format!("{{ {}: {} }}", variant_name, inner));
599                    }
600                    StructKind::TupleStruct => {
601                        // Multi-element tuple variant: { VariantName: [T1, T2, ...] }
602                        let types: Vec<String> = variant
603                            .data
604                            .fields
605                            .iter()
606                            .map(|f| self.type_for_shape(f.shape.get()))
607                            .collect();
608                        variant_types.push(format!(
609                            "{{ {}: [{}] }}",
610                            variant_name,
611                            types.join(", ")
612                        ));
613                    }
614                    _ => {
615                        // Struct variant: { VariantName: { ...fields } }
616                        let field_types = self.collect_inline_fields(variant.data.fields, false);
617                        variant_types.push(format!(
618                            "{{ {}: {{ {} }} }}",
619                            variant_name,
620                            field_types.join("; ")
621                        ));
622                    }
623                }
624            }
625
626            writeln!(
627                self.output,
628                "export type {} =\n  | {};",
629                shape.type_identifier,
630                variant_types.join("\n  | ")
631            )
632            .unwrap();
633        }
634        self.output.push('\n');
635    }
636
637    fn type_for_shape(&mut self, shape: &'static Shape) -> String {
638        // Check Def first - these take precedence over transparent wrappers
639        match &shape.def {
640            Def::Scalar => self.scalar_type(shape),
641            Def::Option(opt) => {
642                format!("{} | null", self.type_for_shape(opt.t))
643            }
644            Def::List(list) => {
645                format!("{}[]", self.type_for_shape(list.t))
646            }
647            Def::Array(arr) => {
648                format!("{}[]", self.type_for_shape(arr.t))
649            }
650            Def::Set(set) => {
651                format!("{}[]", self.type_for_shape(set.t))
652            }
653            Def::Map(map) => {
654                format!("Record<string, {}>", self.type_for_shape(map.v))
655            }
656            Def::Pointer(ptr) => {
657                // Smart pointers are transparent
658                if let Some(pointee) = ptr.pointee {
659                    self.type_for_shape(pointee)
660                } else {
661                    "unknown".to_string()
662                }
663            }
664            Def::Undefined => {
665                // User-defined types - queue for generation and return name
666                match &shape.ty {
667                    Type::User(UserType::Struct(st)) => {
668                        // Handle tuples specially - inline them as [T1, T2, ...] since their
669                        // type_identifier "(…)" is not a valid TypeScript identifier
670                        if st.kind == StructKind::Tuple {
671                            let types: Vec<String> = st
672                                .fields
673                                .iter()
674                                .map(|f| self.type_for_shape(f.shape.get()))
675                                .collect();
676                            format!("[{}]", types.join(", "))
677                        } else {
678                            self.add_shape(shape);
679                            shape.type_identifier.to_string()
680                        }
681                    }
682                    Type::User(UserType::Enum(_)) => {
683                        self.add_shape(shape);
684                        shape.type_identifier.to_string()
685                    }
686                    _ => {
687                        // For other undefined types, check if it's a transparent wrapper
688                        if let Some(inner) = shape.inner {
689                            self.type_for_shape(inner)
690                        } else {
691                            "unknown".to_string()
692                        }
693                    }
694                }
695            }
696            _ => {
697                // For other defs, check if it's a transparent wrapper
698                if let Some(inner) = shape.inner {
699                    self.type_for_shape(inner)
700                } else {
701                    "unknown".to_string()
702                }
703            }
704        }
705    }
706
707    fn scalar_type(&self, shape: &'static Shape) -> String {
708        match shape.type_identifier {
709            // Strings
710            "String" | "str" | "&str" | "Cow" => "string".to_string(),
711
712            // Booleans
713            "bool" => "boolean".to_string(),
714
715            // Numbers (all become number in TypeScript)
716            "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
717            | "i128" | "isize" | "f32" | "f64" => "number".to_string(),
718
719            // Char as string
720            "char" => "string".to_string(),
721
722            // chrono types
723            "NaiveDate"
724            | "NaiveDateTime"
725            | "NaiveTime"
726            | "DateTime<Utc>"
727            | "DateTime<FixedOffset>"
728            | "DateTime<Local>"
729                if shape.module_path == Some("chrono") =>
730            {
731                "string".to_string()
732            }
733
734            // Unknown scalar
735            _ => "unknown".to_string(),
736        }
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743    use alloc::collections::BTreeMap;
744    use facet::Facet;
745
746    #[test]
747    fn test_simple_struct() {
748        #[derive(Facet)]
749        struct User {
750            name: String,
751            age: u32,
752        }
753
754        let ts = to_typescript::<User>();
755        insta::assert_snapshot!(ts);
756    }
757
758    #[test]
759    fn test_optional_field() {
760        #[derive(Facet)]
761        struct Config {
762            required: String,
763            optional: Option<String>,
764        }
765
766        let ts = to_typescript::<Config>();
767        insta::assert_snapshot!(ts);
768    }
769
770    #[test]
771    fn test_simple_enum() {
772        #[derive(Facet)]
773        #[repr(u8)]
774        enum Status {
775            Active,
776            Inactive,
777            Pending,
778        }
779
780        let ts = to_typescript::<Status>();
781        insta::assert_snapshot!(ts);
782    }
783
784    #[test]
785    fn test_vec() {
786        #[derive(Facet)]
787        struct Data {
788            items: Vec<String>,
789        }
790
791        let ts = to_typescript::<Data>();
792        insta::assert_snapshot!(ts);
793    }
794
795    #[test]
796    fn test_nested_types() {
797        #[derive(Facet)]
798        struct Inner {
799            value: i32,
800        }
801
802        #[derive(Facet)]
803        struct Outer {
804            inner: Inner,
805            name: String,
806        }
807
808        let ts = to_typescript::<Outer>();
809        insta::assert_snapshot!(ts);
810    }
811
812    #[test]
813    fn test_enum_rename_all_snake_case() {
814        #[derive(Facet)]
815        #[facet(rename_all = "snake_case")]
816        #[repr(u8)]
817        enum ValidationErrorCode {
818            CircularDependency,
819            InvalidNaming,
820            UnknownRequirement,
821        }
822
823        let ts = to_typescript::<ValidationErrorCode>();
824        insta::assert_snapshot!(ts);
825    }
826
827    #[test]
828    fn test_enum_rename_individual() {
829        #[derive(Facet)]
830        #[repr(u8)]
831        enum GitStatus {
832            #[facet(rename = "dirty")]
833            Dirty,
834            #[facet(rename = "staged")]
835            Staged,
836            #[facet(rename = "clean")]
837            Clean,
838        }
839
840        let ts = to_typescript::<GitStatus>();
841        insta::assert_snapshot!(ts);
842    }
843
844    #[test]
845    fn test_struct_rename_all_camel_case() {
846        #[derive(Facet)]
847        #[facet(rename_all = "camelCase")]
848        struct ApiResponse {
849            user_name: String,
850            created_at: String,
851            is_active: bool,
852        }
853
854        let ts = to_typescript::<ApiResponse>();
855        insta::assert_snapshot!(ts);
856    }
857
858    #[test]
859    fn test_struct_rename_individual() {
860        #[derive(Facet)]
861        struct UserProfile {
862            #[facet(rename = "userName")]
863            user_name: String,
864            #[facet(rename = "emailAddress")]
865            email: String,
866        }
867
868        let ts = to_typescript::<UserProfile>();
869        insta::assert_snapshot!(ts);
870    }
871
872    #[test]
873    fn test_enum_with_data_rename_all() {
874        #[derive(Facet)]
875        #[facet(rename_all = "snake_case")]
876        #[repr(C)]
877        #[allow(dead_code)]
878        enum Message {
879            TextMessage { content: String },
880            ImageUpload { url: String, width: u32 },
881        }
882
883        let ts = to_typescript::<Message>();
884        insta::assert_snapshot!(ts);
885    }
886
887    #[test]
888    fn test_tagged_enum_unit_and_data_variants() {
889        #[derive(Facet)]
890        #[facet(rename_all = "snake_case")]
891        #[repr(u8)]
892        #[allow(dead_code)]
893        enum ResponseStatus {
894            Pending,
895            Ok(String),
896            Error { message: String },
897            Cancelled,
898        }
899
900        let ts = to_typescript::<ResponseStatus>();
901        insta::assert_snapshot!("tagged_enum_unit_and_data_variants", ts);
902    }
903
904    #[test]
905    fn test_struct_with_tuple_field() {
906        #[derive(Facet)]
907        struct Container {
908            coordinates: (i32, i32),
909        }
910
911        let ts = to_typescript::<Container>();
912        insta::assert_snapshot!(ts);
913    }
914
915    #[test]
916    fn test_struct_with_single_element_tuple() {
917        #[derive(Facet)]
918        struct Wrapper {
919            value: (String,),
920        }
921
922        let ts = to_typescript::<Wrapper>();
923        insta::assert_snapshot!(ts);
924    }
925
926    #[test]
927    fn test_enum_with_tuple_variant() {
928        #[derive(Facet)]
929        #[repr(C)]
930        #[allow(dead_code)]
931        enum Event {
932            Click { x: i32, y: i32 },
933            Move((i32, i32)),
934            Resize { dimensions: (u32, u32) },
935        }
936
937        let ts = to_typescript::<Event>();
938        insta::assert_snapshot!(ts);
939    }
940
941    #[test]
942    fn test_untagged_enum() {
943        #[derive(Facet)]
944        #[facet(untagged)]
945        #[repr(C)]
946        #[allow(dead_code)]
947        pub enum Value {
948            Text(String),
949            Number(f64),
950        }
951
952        let ts = to_typescript::<Value>();
953        insta::assert_snapshot!(ts);
954    }
955
956    #[test]
957    fn test_untagged_enum_unit_and_struct_variants() {
958        #[derive(Facet)]
959        #[facet(untagged)]
960        #[repr(C)]
961        #[allow(dead_code)]
962        pub enum Event {
963            None,
964            Data { x: i32, y: i32 },
965        }
966
967        let ts = to_typescript::<Event>();
968        insta::assert_snapshot!(ts);
969    }
970
971    #[test]
972    fn test_enum_with_tuple_struct_variant() {
973        #[derive(Facet)]
974        #[allow(dead_code)]
975        pub struct Point {
976            x: f64,
977            y: f64,
978        }
979
980        #[derive(Facet)]
981        #[repr(u8)]
982        #[allow(dead_code)]
983        pub enum Shape {
984            Line(Point, Point),
985        }
986
987        let ts = to_typescript::<Shape>();
988        insta::assert_snapshot!(ts);
989    }
990
991    #[test]
992    fn test_enum_with_proxy_struct() {
993        #[derive(Facet)]
994        #[facet(proxy = PointProxy)]
995        #[allow(dead_code)]
996        pub struct Point {
997            xxx: f64,
998            yyy: f64,
999        }
1000
1001        #[derive(Facet)]
1002        #[allow(dead_code)]
1003        pub struct PointProxy {
1004            x: f64,
1005            y: f64,
1006        }
1007
1008        impl From<PointProxy> for Point {
1009            fn from(p: PointProxy) -> Self {
1010                Self { xxx: p.x, yyy: p.y }
1011            }
1012        }
1013
1014        impl From<&Point> for PointProxy {
1015            fn from(p: &Point) -> Self {
1016                Self { x: p.xxx, y: p.yyy }
1017            }
1018        }
1019
1020        #[derive(Facet)]
1021        #[repr(u8)]
1022        #[facet(untagged)]
1023        #[allow(dead_code)]
1024        pub enum Shape {
1025            Circle { center: Point, radius: f64 },
1026            Line(Point, Point),
1027        }
1028
1029        let ts = to_typescript::<Shape>();
1030        insta::assert_snapshot!(ts);
1031    }
1032
1033    #[test]
1034    fn test_enum_with_proxy_enum() {
1035        #[derive(Facet)]
1036        #[repr(u8)]
1037        #[facet(proxy = StatusProxy)]
1038        pub enum Status {
1039            Unknown,
1040        }
1041
1042        #[derive(Facet)]
1043        #[repr(u8)]
1044        pub enum StatusProxy {
1045            Active,
1046            Inactive,
1047        }
1048
1049        impl From<StatusProxy> for Status {
1050            fn from(_: StatusProxy) -> Self {
1051                Self::Unknown
1052            }
1053        }
1054
1055        impl From<&Status> for StatusProxy {
1056            fn from(_: &Status) -> Self {
1057                Self::Active
1058            }
1059        }
1060
1061        let ts = to_typescript::<Status>();
1062        insta::assert_snapshot!(ts);
1063    }
1064
1065    #[test]
1066    fn test_proxy_to_scalar() {
1067        /// A user ID that serializes as a string
1068        #[derive(Facet)]
1069        #[facet(proxy = String)]
1070        #[allow(dead_code)]
1071        pub struct UserId(u64);
1072
1073        impl From<String> for UserId {
1074            fn from(s: String) -> Self {
1075                Self(s.parse().unwrap_or(0))
1076            }
1077        }
1078
1079        impl From<&UserId> for String {
1080            fn from(id: &UserId) -> Self {
1081                id.0.to_string()
1082            }
1083        }
1084
1085        let ts = to_typescript::<UserId>();
1086        insta::assert_snapshot!(ts);
1087    }
1088
1089    #[test]
1090    fn test_proxy_preserves_doc_comments() {
1091        /// This is a point in 2D space.
1092        /// It has x and y coordinates.
1093        #[derive(Facet)]
1094        #[facet(proxy = PointProxy)]
1095        #[allow(dead_code)]
1096        pub struct Point {
1097            internal_x: f64,
1098            internal_y: f64,
1099        }
1100
1101        #[derive(Facet)]
1102        #[allow(dead_code)]
1103        pub struct PointProxy {
1104            x: f64,
1105            y: f64,
1106        }
1107
1108        impl From<PointProxy> for Point {
1109            fn from(p: PointProxy) -> Self {
1110                Self {
1111                    internal_x: p.x,
1112                    internal_y: p.y,
1113                }
1114            }
1115        }
1116
1117        impl From<&Point> for PointProxy {
1118            fn from(p: &Point) -> Self {
1119                Self {
1120                    x: p.internal_x,
1121                    y: p.internal_y,
1122                }
1123            }
1124        }
1125
1126        let ts = to_typescript::<Point>();
1127        insta::assert_snapshot!(ts);
1128    }
1129
1130    #[test]
1131    fn test_untagged_enum_optional_fields() {
1132        #[derive(Facet)]
1133        #[facet(untagged)]
1134        #[repr(C)]
1135        #[allow(dead_code)]
1136        pub enum Config {
1137            Simple {
1138                name: String,
1139            },
1140            Full {
1141                name: String,
1142                description: Option<String>,
1143                count: Option<u32>,
1144            },
1145        }
1146
1147        let ts = to_typescript::<Config>();
1148        insta::assert_snapshot!(ts);
1149    }
1150
1151    #[test]
1152    fn test_flatten_variants() {
1153        use std::sync::Arc;
1154
1155        // Inner struct with a skipped field to test skip handling
1156        #[derive(Facet)]
1157        pub struct Coords {
1158            pub x: i32,
1159            pub y: i32,
1160            #[facet(skip)]
1161            pub internal: u8,
1162        }
1163
1164        // Direct flatten
1165        #[derive(Facet)]
1166        pub struct FlattenDirect {
1167            pub name: String,
1168            #[facet(flatten)]
1169            pub coords: Coords,
1170        }
1171
1172        // Flatten through Arc<T>
1173        #[derive(Facet)]
1174        pub struct FlattenArc {
1175            pub name: String,
1176            #[facet(flatten)]
1177            pub coords: Arc<Coords>,
1178        }
1179
1180        // Flatten through Box<T>
1181        #[derive(Facet)]
1182        pub struct FlattenBox {
1183            pub name: String,
1184            #[facet(flatten)]
1185            pub coords: Box<Coords>,
1186        }
1187
1188        // Flatten Option<T> makes inner fields optional
1189        #[derive(Facet)]
1190        pub struct FlattenOption {
1191            pub name: String,
1192            #[facet(flatten)]
1193            pub coords: Option<Coords>,
1194        }
1195
1196        // Nested Option<Arc<T>> tests multi-layer unwrapping
1197        #[derive(Facet)]
1198        pub struct FlattenOptionArc {
1199            pub name: String,
1200            #[facet(flatten)]
1201            pub coords: Option<Arc<Coords>>,
1202        }
1203
1204        // Non-struct flatten (BTreeMap) falls through to normal field output
1205        #[derive(Facet)]
1206        pub struct FlattenMap {
1207            pub name: String,
1208            #[facet(flatten)]
1209            pub extra: BTreeMap<String, String>,
1210        }
1211
1212        let ts_direct = to_typescript::<FlattenDirect>();
1213        let ts_arc = to_typescript::<FlattenArc>();
1214        let ts_box = to_typescript::<FlattenBox>();
1215        let ts_option = to_typescript::<FlattenOption>();
1216        let ts_option_arc = to_typescript::<FlattenOptionArc>();
1217        let ts_map = to_typescript::<FlattenMap>();
1218
1219        insta::assert_snapshot!("flatten_direct", ts_direct);
1220        insta::assert_snapshot!("flatten_arc", ts_arc);
1221        insta::assert_snapshot!("flatten_box", ts_box);
1222        insta::assert_snapshot!("flatten_option", ts_option);
1223        insta::assert_snapshot!("flatten_option_arc", ts_option_arc);
1224        insta::assert_snapshot!("flatten_map", ts_map);
1225    }
1226
1227    #[test]
1228    fn test_tagged_enum_optional_fields() {
1229        #[derive(Facet)]
1230        #[repr(u8)]
1231        #[allow(dead_code)]
1232        enum Message {
1233            Simple {
1234                text: String,
1235            },
1236            Full {
1237                text: String,
1238                metadata: Option<String>,
1239                count: Option<u32>,
1240            },
1241        }
1242
1243        let ts = to_typescript::<Message>();
1244        insta::assert_snapshot!(ts);
1245    }
1246
1247    #[test]
1248    fn test_flatten_proxy_struct() {
1249        #[derive(Facet)]
1250        #[facet(proxy = CoordsProxy)]
1251        #[allow(dead_code)]
1252        struct Coords {
1253            internal_x: f64,
1254            internal_y: f64,
1255        }
1256
1257        #[derive(Facet)]
1258        #[allow(dead_code)]
1259        struct CoordsProxy {
1260            x: f64,
1261            y: f64,
1262        }
1263
1264        impl From<CoordsProxy> for Coords {
1265            fn from(p: CoordsProxy) -> Self {
1266                Self {
1267                    internal_x: p.x,
1268                    internal_y: p.y,
1269                }
1270            }
1271        }
1272
1273        impl From<&Coords> for CoordsProxy {
1274            fn from(c: &Coords) -> Self {
1275                Self {
1276                    x: c.internal_x,
1277                    y: c.internal_y,
1278                }
1279            }
1280        }
1281
1282        #[derive(Facet)]
1283        #[allow(dead_code)]
1284        struct Shape {
1285            name: String,
1286            #[facet(flatten)]
1287            coords: Coords,
1288        }
1289
1290        let ts = to_typescript::<Shape>();
1291        insta::assert_snapshot!(ts);
1292    }
1293
1294    #[test]
1295    fn test_enum_variant_skipped_field() {
1296        #[derive(Facet)]
1297        #[repr(u8)]
1298        #[allow(dead_code)]
1299        enum Event {
1300            Data {
1301                visible: String,
1302                #[facet(skip)]
1303                internal: u64,
1304            },
1305        }
1306
1307        let ts = to_typescript::<Event>();
1308        insta::assert_snapshot!(ts);
1309    }
1310
1311    #[test]
1312    fn test_enum_variant_flatten() {
1313        // BUG: Enum struct variants should inline flattened fields
1314        #[derive(Facet)]
1315        #[allow(dead_code)]
1316        struct Metadata {
1317            author: String,
1318            version: u32,
1319        }
1320
1321        #[derive(Facet)]
1322        #[repr(u8)]
1323        #[allow(dead_code)]
1324        enum Document {
1325            Article {
1326                title: String,
1327                #[facet(flatten)]
1328                meta: Metadata,
1329            },
1330        }
1331
1332        let ts = to_typescript::<Document>();
1333        insta::assert_snapshot!(ts);
1334    }
1335
1336    #[test]
1337    fn test_nested_flatten_struct() {
1338        #[derive(Facet)]
1339        #[allow(dead_code)]
1340        struct Inner {
1341            x: i32,
1342            y: i32,
1343        }
1344
1345        #[derive(Facet)]
1346        #[allow(dead_code)]
1347        struct Middle {
1348            #[facet(flatten)]
1349            inner: Inner,
1350            z: i32,
1351        }
1352
1353        #[derive(Facet)]
1354        #[allow(dead_code)]
1355        struct Outer {
1356            name: String,
1357            #[facet(flatten)]
1358            middle: Middle,
1359        }
1360
1361        let ts = to_typescript::<Outer>();
1362        insta::assert_snapshot!(ts);
1363    }
1364
1365    #[test]
1366    fn test_flatten_recursive_option_box() {
1367        #[derive(Facet)]
1368        struct Node {
1369            value: u32,
1370            #[facet(flatten)]
1371            next: Option<Box<Node>>,
1372        }
1373
1374        let ts = to_typescript::<Node>();
1375        insta::assert_snapshot!("flatten_recursive_option_box", ts);
1376    }
1377
1378    #[test]
1379    fn test_skip_serializing_struct_field() {
1380        #[derive(Facet)]
1381        struct Data {
1382            visible: String,
1383            #[facet(skip_serializing)]
1384            internal: u64,
1385        }
1386
1387        let ts = to_typescript::<Data>();
1388        insta::assert_snapshot!("skip_serializing_struct_field", ts);
1389    }
1390
1391    #[test]
1392    fn test_skip_serializing_inline_enum_variant_and_flatten_cycle_guard() {
1393        #[derive(Facet)]
1394        struct Node {
1395            value: u32,
1396            #[facet(flatten)]
1397            next: Option<Box<Node>>,
1398        }
1399
1400        #[derive(Facet)]
1401        #[repr(u8)]
1402        enum Wrapper {
1403            Item {
1404                #[facet(flatten)]
1405                node: Node,
1406            },
1407            Data {
1408                visible: String,
1409                #[facet(skip_serializing)]
1410                internal: u64,
1411            },
1412        }
1413
1414        let item = Wrapper::Item {
1415            node: Node {
1416                value: 1,
1417                next: None,
1418            },
1419        };
1420        match item {
1421            Wrapper::Item { node } => assert_eq!(node.value, 1),
1422            Wrapper::Data { .. } => unreachable!(),
1423        }
1424
1425        let data = Wrapper::Data {
1426            visible: String::new(),
1427            internal: 0,
1428        };
1429        match data {
1430            Wrapper::Data { visible, internal } => {
1431                assert!(visible.is_empty());
1432                assert_eq!(internal, 0);
1433            }
1434            Wrapper::Item { .. } => unreachable!(),
1435        }
1436
1437        let ts = to_typescript::<Wrapper>();
1438        insta::assert_snapshot!(
1439            "skip_serializing_inline_enum_variant_and_flatten_cycle_guard",
1440            ts
1441        );
1442    }
1443
1444    #[test]
1445    fn test_empty_struct() {
1446        #[derive(Facet)]
1447        struct Data {
1448            empty: Empty,
1449        }
1450
1451        #[derive(Facet)]
1452        struct Empty {}
1453
1454        let e = to_typescript::<Empty>();
1455        let d = to_typescript::<Data>();
1456        insta::assert_snapshot!("test_empty_struct", e);
1457        insta::assert_snapshot!("test_empty_struct_wrap", d);
1458    }
1459
1460    #[test]
1461    fn test_empty_struct_with_skipped_fields() {
1462        #[derive(Facet)]
1463        struct EmptyAfterSkip {
1464            #[facet(skip_serializing)]
1465            internal: String,
1466        }
1467
1468        let ts = to_typescript::<EmptyAfterSkip>();
1469        insta::assert_snapshot!("test_empty_struct_with_skipped_fields", ts);
1470    }
1471
1472    #[test]
1473    fn test_empty_struct_multiple_references() {
1474        #[derive(Facet)]
1475        struct Container {
1476            first: Empty,
1477            second: Empty,
1478            third: Option<Empty>,
1479        }
1480
1481        #[derive(Facet)]
1482        struct Empty {}
1483
1484        let ts = to_typescript::<Container>();
1485        insta::assert_snapshot!("test_empty_struct_multiple_references", ts);
1486    }
1487
1488    #[test]
1489    fn test_flatten_empty_struct() {
1490        #[derive(Facet)]
1491        struct Empty {}
1492
1493        #[derive(Facet)]
1494        struct Wrapper {
1495            #[facet(flatten)]
1496            empty: Empty,
1497        }
1498
1499        let ts = to_typescript::<Wrapper>();
1500        insta::assert_snapshot!("test_flatten_empty_struct", ts);
1501    }
1502
1503    #[test]
1504    fn test_default_not_required() {
1505        #[derive(Facet, Default)]
1506        struct Def {
1507            pub a: i32,
1508            pub b: i32,
1509        }
1510
1511        #[derive(Facet)]
1512        struct Wrapper {
1513            pub a: String,
1514            #[facet(default)]
1515            pub d: Def,
1516        }
1517
1518        let ts = to_typescript::<Wrapper>();
1519        insta::assert_snapshot!("test_default_not_required", ts);
1520    }
1521
1522    #[test]
1523    fn test_default_mixed_fields() {
1524        #[derive(Facet)]
1525        struct MixedDefaults {
1526            pub required: String,
1527            pub optional: Option<String>,
1528            #[facet(default)]
1529            pub with_default: i32,
1530            #[facet(default = 100)]
1531            pub with_default_expr: i32,
1532            #[facet(default)]
1533            pub option_with_default: Option<String>,
1534        }
1535
1536        let ts = to_typescript::<MixedDefaults>();
1537        insta::assert_snapshot!("test_default_mixed_fields", ts);
1538    }
1539
1540    #[test]
1541    fn test_default_in_flattened_struct() {
1542        #[derive(Facet)]
1543        struct FlattenedInner {
1544            pub foo: String,
1545            #[facet(default)]
1546            pub bar: u32,
1547        }
1548
1549        #[derive(Facet)]
1550        struct WithFlatten {
1551            pub outer_field: String,
1552            #[facet(flatten)]
1553            pub inner: FlattenedInner,
1554        }
1555
1556        let ts = to_typescript::<WithFlatten>();
1557        insta::assert_snapshot!("test_default_in_flattened_struct", ts);
1558    }
1559
1560    #[test]
1561    fn test_default_in_enum_variant() {
1562        #[derive(Facet)]
1563        #[allow(dead_code)]
1564        #[repr(C)]
1565        enum Message {
1566            Text {
1567                content: String,
1568            },
1569            Data {
1570                required: String,
1571                #[facet(default)]
1572                optional: i32,
1573            },
1574        }
1575
1576        let ts = to_typescript::<Message>();
1577        insta::assert_snapshot!("test_default_in_enum_variant", ts);
1578    }
1579
1580    #[test]
1581    fn test_untagged_enum_unit_and_newtype_variants() {
1582        #[derive(Facet, Clone, PartialEq, PartialOrd)]
1583        #[repr(C)]
1584        #[allow(dead_code)]
1585        #[facet(untagged)]
1586        pub enum Enum {
1587            Daily,
1588            Weekly,
1589            Custom(f64),
1590        }
1591
1592        let ts = to_typescript::<Enum>();
1593        insta::assert_snapshot!("test_untagged_enum_unit_and_newtype_variants", ts);
1594    }
1595
1596    #[test]
1597    fn test_untagged_enum_with_tuple_variant() {
1598        #[derive(Facet)]
1599        #[repr(C)]
1600        #[allow(dead_code)]
1601        #[facet(untagged)]
1602        pub enum Message {
1603            Text(String),
1604            Pair(String, i32),
1605            Struct { x: i32, y: i32 },
1606        }
1607
1608        let ts = to_typescript::<Message>();
1609        insta::assert_snapshot!("test_untagged_enum_with_tuple_variant", ts);
1610    }
1611    #[test]
1612    fn test_chrono_naive_date() {
1613        use chrono::NaiveDate;
1614
1615        #[derive(Facet)]
1616        struct WithChronoDate {
1617            birthday: NaiveDate,
1618        }
1619
1620        let ts = to_typescript::<WithChronoDate>();
1621        insta::assert_snapshot!(ts);
1622    }
1623
1624    #[test]
1625    fn test_non_transparent_newtype_is_not_scalar_alias() {
1626        #[derive(Facet)]
1627        struct Envelope {
1628            id: BacktraceId,
1629        }
1630
1631        #[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
1632        struct BacktraceId(u64);
1633
1634        let mut ts_gen = TypeScriptGenerator::new();
1635        ts_gen.add_type::<Envelope>();
1636        let out = ts_gen.finish();
1637
1638        assert!(
1639            !out.contains("export type BacktraceId = number;"),
1640            "bug: non-transparent tuple newtype generated scalar alias:\n{out}"
1641        );
1642        insta::assert_snapshot!("non_transparent_newtype", out);
1643    }
1644
1645    #[test]
1646    fn test_transparent_newtype_is_scalar_alias() {
1647        #[derive(Facet, Debug, Clone, Copy, PartialEq, Eq, Hash)]
1648        #[facet(transparent)]
1649        struct TransparentId(u64);
1650
1651        let ts = to_typescript::<TransparentId>();
1652        assert!(
1653            ts.contains("export type TransparentId = number;"),
1654            "bug: transparent tuple newtype did not generate scalar alias:\n{ts}"
1655        );
1656        insta::assert_snapshot!("transparent_newtype", ts);
1657    }
1658
1659    #[test]
1660    fn test_internally_tagged_enum() {
1661        #[derive(Facet)]
1662        #[facet(tag = "type")]
1663        #[repr(C)]
1664        #[allow(dead_code)]
1665        enum Rr {
1666            Mat,
1667            Sp { first_roll: u32, long_last: bool },
1668        }
1669
1670        let ts = to_typescript::<Rr>();
1671        insta::assert_snapshot!(ts);
1672    }
1673
1674    // Non-transparent single-field tuple struct serializes as [42] in facet-json,
1675    // so TypeScript must emit [number], not number.
1676    #[test]
1677    fn test_non_transparent_single_field_tuple_struct() {
1678        #[derive(Facet)]
1679        struct Spread(pub i32);
1680
1681        let ts = to_typescript::<Spread>();
1682        assert!(
1683            ts.contains("export type Spread = [number];"),
1684            "bug: non-transparent single-field tuple struct should be [number], got:\n{ts}"
1685        );
1686        insta::assert_snapshot!(ts);
1687    }
1688}