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