Skip to main content

specta_typescript/
semantic.rs

1//! Runtime-aware TypeScript type remapping.
2//!
3//! Semantic types are Rust types whose TypeScript runtime value should be more
4//! specific than their JSON-compatible wire representation.
5//!
6//! This enables the following default rules:
7//!  - [`bytes::Bytes`](https://docs.rs/bytes/latest/bytes/struct.Bytes.html) to become [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)
8//!  - [`bytes::BytesMut`](https://docs.rs/bytes/latest/bytes/struct.BytesMut.html) to become [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)
9//!  - [`url::Url`](https://docs.rs/url/latest/url/struct.Url.html) to become [`Url`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL)
10//!  - [`chrono::DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) to become [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
11//!  - [`chrono::NaiveDate`](https://docs.rs/chrono/latest/chrono/struct.NaiveDate.html) to become [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
12//!  - [`jiff::Timestamp`](https://docs.rs/jiff/latest/jiff/struct.Timestamp.html) to become [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
13//!  - [`jiff::civil::Date`](https://docs.rs/jiff/latest/jiff/civil/struct.Date.html) to become [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date)
14//!
15//! This could also allow you to map your own Rust types into custom JavaScript types like custom classes. Refer to [`semantic::Configuration::define`](Configuration::define) for building your own rules.
16//!
17//! This is intended the be implemented by frameworks like [Tauri Specta](https://github.com/specta-rs/tauri-specta), [TauRPC](https://github.com/MatsDK/TauRPC) and [rspc](https://github.com/specta-rs/rspc) as they have control of the runtime and type layer.
18//!
19//! <div class="warning">
20//!
21//! **WARNING:** The current implementation relies on the frontend and backend being versioned in-step. This works for a Tauri desktop application but may become an issue for a HTTP API unless you have something like [Skew Protection](https://vercel.com/docs/skew-protection).
22//!
23//! We will likely lift this as a hard restriction in the future!
24//!
25//! </div>
26//!
27//! <details>
28//! <summary>Implementing into your own framework</summary>
29//!
30//! # Implementing into your own framework
31//!
32//! A framework needs to be integrated properly for this feature to work, as it requires both type-level and runtime JS to make it work properly.
33//!
34//! I would highly recommend reading [specta-rs/specta#203](https://github.com/specta-rs/specta/issues/203) and understanding it as it's the core work which inspired this feature.
35//!
36//! Documentation coming soon... For now refer to [specta-rs/tauri-specta#219](https://github.com/specta-rs/tauri-specta/pull/219) which was the original implementation into [Tauri Specta](https://github.com/specta-rs/tauri-specta).
37//!
38//! </details>
39//!
40
41use std::{borrow::Cow, fmt, sync::Arc};
42
43use specta::{
44    Type, Types,
45    datatype::{DataType, Fields, NamedReferenceType, Primitive, Reference},
46};
47
48use crate::{
49    define,
50    primitives::{escape_typescript_string_literal, is_identifier},
51};
52
53/// A JavaScript expression that converts between a semantic
54/// TypeScript runtime value and its JSON-compatible representation.
55///
56/// The closure receives the JavaScript identifier/expression being transformed
57/// and must return a JavaScript expression using that value.
58///
59/// # Examples
60///
61/// Convert a JSON string into a TypeScript `Date`:
62///
63/// ```rust
64/// use specta_typescript::semantic::Transform;
65///
66/// let transform = Transform::new(|value| format!("new Date({value})"));
67/// # let _ = transform;
68/// ```
69///
70/// Convert a `Uint8Array` into a JSON array of numbers:
71///
72/// ```rust
73/// use specta_typescript::semantic::Transform;
74///
75/// let transform = Transform::new(|value| format!("[...{value}]"));
76/// # let _ = transform;
77/// ```
78///
79/// Use [`Transform::identity`] when the TypeScript runtime value already has
80/// the same representation as the value crossing the wire or when JSON.stringify/JSON.parse
81/// is already able to handle the transformation for you.
82#[derive(Clone)]
83#[non_exhaustive]
84pub struct Transform(
85    /// The runtime transform function
86    ///
87    /// This is called with the argument being a Typescript identifier.
88    /// It should output some transformation on the identifier.
89    /// Eg. `|i| format!("new Date({i})")` could be one valid implementation.
90    Option<Arc<dyn Fn(&str) -> String + Send + Sync>>,
91);
92
93impl fmt::Debug for Transform {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        match &self.0 {
96            Some(r) => write!(f, "{r:p}"),
97            None => write!(f, "<none>"),
98        }
99    }
100}
101
102impl Transform {
103    /// Construct a runtime transform from a JavaScript identifier mapper.
104    ///
105    /// The mapper should return a JavaScript expression, not a statement.
106    ///
107    /// ```rust
108    /// use specta_typescript::semantic::Transform;
109    ///
110    /// let transform = Transform::new(|ident| format!("new URL({ident})"));
111    /// # let _ = transform;
112    /// ```
113    pub fn new(runtime: impl Fn(&str) -> String + Send + Sync + 'static) -> Self {
114        Self(Some(Arc::new(runtime)))
115    }
116
117    /// Construct an identity runtime transform.
118    ///
119    /// This is useful when a rule only changes the exported TypeScript type, or
120    /// when one direction does not need runtime conversion.
121    ///
122    /// ```rust
123    /// use specta_typescript::semantic::Transform;
124    ///
125    /// let transform = Transform::identity();
126    /// # let _ = transform;
127    /// ```
128    pub fn identity() -> Self {
129        Self(None)
130    }
131
132    fn apply(&self, ident: &str) -> String {
133        match &self.0 {
134            Some(runtime) => runtime(ident),
135            None => ident.to_owned(),
136        }
137    }
138}
139
140#[derive(Clone)]
141pub(crate) struct DataTypeFn(Arc<dyn Fn(DataType) -> DataType + Send + Sync>);
142
143impl DataTypeFn {
144    pub(crate) fn new(f: impl Fn(DataType) -> DataType + Send + Sync + 'static) -> Self {
145        Self(Arc::new(f))
146    }
147}
148
149impl fmt::Debug for DataTypeFn {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        f.debug_tuple("DataTypeFn")
152            .field(&format!("{:p}", self.0))
153            .finish()
154    }
155}
156
157/// A rule for a specific named data type.
158#[derive(Debug, Clone)]
159#[non_exhaustive]
160pub struct Rule {
161    /// Matched against [`NamedDataType::name`](specta::datatype::NamedDataType::name) to determine if rule should apply
162    pub name: Cow<'static, str>,
163    /// Matched against [`NamedDataType::module_path`](specta::datatype::NamedDataType::module_path) to determine if rule should apply
164    pub module_path: Cow<'static, str>,
165    /// The type transformation function
166    ///
167    /// This must match up with the type produced or consumed by the runtime.
168    pub(crate) data_type: DataTypeFn,
169    /// The type transformation for serializing.
170    /// This is JS -> Rust
171    pub(crate) serialize: Option<Transform>,
172    /// The type transformation for deserializing.
173    /// This is Rust -> JS
174    pub(crate) deserialize: Option<Transform>,
175}
176
177/// Configuration for runtime-aware TypeScript type remapping.
178///
179/// By default this contains a set of default rules as defined on [the module](crate::semantic). If you don't want them use [`Configuration::empty()`](Configuration::empty) instead.
180///
181/// You can add your own rules via [`Configuration::define(...)`](Configuration::define).
182///
183#[derive(Debug, Clone)]
184pub struct Configuration {
185    rules: Vec<Rule>,
186    lossless_bigint: bool,
187    lossless_floats: bool,
188}
189
190impl Default for Configuration {
191    fn default() -> Self {
192        Self {
193            rules: vec![
194                // Uint8Array
195                Rule {
196                    name: "Bytes".into(),
197                    module_path: "bytes".into(),
198                    data_type: DataTypeFn::new(|_| define("Uint8Array").into()),
199                    serialize: Some(Transform::new(|i| format!("[...{i}]"))),
200                    deserialize: Some(Transform::new(|i| format!("new Uint8Array({i})"))),
201                },
202                Rule {
203                    name: "BytesMut".into(),
204                    module_path: "bytes".into(),
205                    data_type: DataTypeFn::new(|_| define("Uint8Array").into()),
206                    serialize: Some(Transform::new(|i| format!("[...{i}]"))),
207                    deserialize: Some(Transform::new(|i| format!("new Uint8Array({i})"))),
208                },
209                // URL
210                Rule {
211                    name: "Url".into(),
212                    module_path: "url".into(),
213                    data_type: DataTypeFn::new(|_| define("URL").into()),
214                    serialize: None,
215                    deserialize: Some(Transform::new(|i| format!("new URL({i})"))),
216                },
217                // Date
218                Rule {
219                    name: "DateTime".into(),
220                    module_path: "chrono".into(),
221                    data_type: DataTypeFn::new(|_| define("Date").into()),
222                    serialize: None,
223                    deserialize: Some(Transform::new(|i| format!("new Date({i})"))),
224                },
225                Rule {
226                    name: "NaiveDate".into(),
227                    module_path: "chrono".into(),
228                    data_type: DataTypeFn::new(|_| define("Date").into()),
229                    serialize: Some(Transform::new(|i| {
230                        format!("{i}.toISOString().slice(0, 10)")
231                    })),
232                    deserialize: Some(Transform::new(|i| format!("new Date({i})"))),
233                },
234                Rule {
235                    name: "Timestamp".into(),
236                    module_path: "jiff".into(),
237                    data_type: DataTypeFn::new(|_| define("Date").into()),
238                    serialize: Some(Transform::new(|i| format!("{i}.toISOString()"))),
239                    deserialize: Some(Transform::new(|i| format!("new Date({i})"))),
240                },
241                Rule {
242                    name: "Date".into(),
243                    module_path: "jiff::civil".into(),
244                    data_type: DataTypeFn::new(|_| define("Date").into()),
245                    serialize: Some(Transform::new(|i| {
246                        format!("{i}.toISOString().slice(0, 10)")
247                    })),
248                    deserialize: Some(Transform::new(|i| format!("new Date({i})"))),
249                },
250                // We don't implement support for `chrono::NaiveDateTime`, and many `jiff` types as lack of timezone is an issue with JS's `Date`
251            ],
252            lossless_bigint: false,
253            lossless_floats: false,
254        }
255    }
256}
257
258impl Configuration {
259    /// Construct a [`Configuration`] without the default rules.
260    ///
261    /// Prefer [`Configuration::default`] when possible; the default
262    /// rules cover common ecosystem types and may grow over time.
263    pub fn empty() -> Self {
264        Self {
265            rules: Default::default(),
266            lossless_bigint: false,
267            lossless_floats: false,
268        }
269    }
270
271    /// Exposes the rules applies to this instance for manual manipulation.
272    ///
273    /// This could be used to filter the default rules if you want to exclude certain ones.
274    pub fn rules_mut(&mut self) -> &mut Vec<Rule> {
275        &mut self.rules
276    }
277
278    /// Define a new rule for a given type `T`.
279    ///
280    /// `dt` receives the original [`DataType`] for `T` and must return the
281    /// TypeScript-facing [`DataType`] that should replace it. `serialize`
282    /// transforms TypeScript runtime values before sending them to Rust.
283    /// `deserialize` transforms values received from Rust into TypeScript
284    /// runtime values.
285    ///
286    /// This only works for named types, such as types generated by the
287    /// [`Type`] derive macro. It does not work for primitives.
288    ///
289    /// ```rust
290    /// use specta::Type;
291    /// use specta_typescript::{define, semantic::{Configuration, Transform}};
292    ///
293    /// #[derive(Type)]
294    /// struct MyCustomUrl(String);
295    ///
296    /// let mut semantic_types = Configuration::empty();
297    /// semantic_types.define::<MyCustomUrl>(
298    ///     |_| define("URL").into(), // Runtime Specta Type
299    ///     Some(Transform::new(|value| format!("{value}.toString()"))), // JS -> JSON
300    ///     Some(Transform::new(|value| format!("new URL({value})"))), // JSON -> JS
301    /// );
302    /// ```
303    pub fn define<T: Type>(
304        mut self,
305        dt: impl Fn(DataType) -> DataType + Send + Sync + 'static,
306        serialize: Option<Transform>,
307        deserialize: Option<Transform>,
308    ) -> Self {
309        let mut types = Types::default();
310        let ndt = match T::definition(&mut types) {
311            DataType::Reference(Reference::Named(r)) => types.get(&r),
312            _ => None,
313        };
314        if let Some(ndt) = ndt {
315            self.rules.push(Rule {
316                name: ndt.name.clone(),
317                module_path: ndt.module_path.clone(),
318                data_type: DataTypeFn(Arc::new(dt)),
319                serialize,
320                deserialize,
321            });
322        }
323
324        self
325    }
326
327    /// Enable lossless support for large integer types (`BigInt`s).
328    ///
329    /// This remaps `usize`, `isize`, `u64`, `i64`, `u128`, and `i128` so they
330    /// are a [`BigInt`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) in JavaScript.
331    ///
332    /// This is only safe if your serialization and deserialization layer can losslessly transmit `BigInt`s to the frontend.
333    ///
334    /// Refer to [specta-rs/specta#203](https://github.com/specta-rs/specta/issues/203) for implementation details.
335    pub fn enable_lossless_bigints(mut self) -> Self {
336        if !self.lossless_bigint {
337            self.lossless_bigint = true;
338        }
339
340        self
341    }
342
343    /// Enable lossless float support.
344    ///
345    /// By enabling this, you assert that your runtime *must* preserve `NaN`,
346    /// `Infinity`, and `-Infinity` values from JavaScript to Rust and we will flatten `number | null` into `number`.
347    ///
348    /// Refer to [specta-rs/specta#203](https://github.com/specta-rs/specta/issues/203) for implementation details.
349    pub fn enable_lossless_floats(mut self) -> Self {
350        if !self.lossless_floats {
351            self.lossless_floats = true;
352        }
353
354        self
355    }
356
357    /// Transform a [`Types`] collection using the configured rules.
358    ///
359    /// This rewrites registered named types so their exported TypeScript shapes
360    /// match the values produced or consumed by the runtime transforms.
361    ///
362    /// Call this after any format-specific mapping that changes the type graph,
363    /// and before exporting the final TypeScript definitions.
364    pub fn apply_types<'a>(&self, types: &'a Types) -> Cow<'a, Types> {
365        let mut types = Cow::Borrowed(types);
366
367        if self.has_builtin_remaps() {
368            types = Cow::Owned(types.into_owned().map(|mut ndt| {
369                let remap_bigint = if ndt.name.ends_with("_Serialize") {
370                    serialize_bigint
371                } else {
372                    deserialize_bigint
373                };
374
375                ndt.generics.to_mut().iter_mut().for_each(|generic| {
376                    if let Some(dt) = &mut generic.default {
377                        apply_builtin_remaps(
378                            dt,
379                            remap_bigint,
380                            self.lossless_bigint,
381                            self.lossless_floats,
382                        );
383                    }
384                });
385                if let Some(dt) = &mut ndt.ty {
386                    apply_builtin_remaps(
387                        dt,
388                        remap_bigint,
389                        self.lossless_bigint,
390                        self.lossless_floats,
391                    );
392                }
393
394                ndt
395            }));
396        }
397
398        if !self.rules.is_empty() {
399            let source = types.into_owned();
400            let lookup = source.clone();
401            types = Cow::Owned(source.map(|mut ndt| {
402                if let Some(dt) = &mut ndt.ty {
403                    self.apply_rules_to_dt(&lookup, dt);
404                }
405
406                if let Some(rule) = self
407                    .rules
408                    .iter()
409                    .find(|r| r.name == ndt.name && r.module_path == ndt.module_path)
410                    && let Some(dt) = ndt.ty.take()
411                {
412                    ndt.ty = Some((rule.data_type.0)(dt));
413                }
414
415                ndt
416            }));
417        }
418
419        types
420    }
421
422    fn apply_rules_to_dt(&self, types: &Types, dt: &mut DataType) {
423        if let DataType::Reference(Reference::Named(reference)) = dt
424            && let Some(rule) = self.rule_for_reference(types, reference)
425        {
426            *dt = (rule.data_type.0)(Self::reference_source_dt(types, reference));
427            return;
428        }
429
430        match dt {
431            DataType::Primitive(_) | DataType::Generic(_) => {}
432            DataType::List(list) => self.apply_rules_to_dt(types, &mut list.ty),
433            DataType::Map(map) => {
434                self.apply_rules_to_dt(types, map.key_ty_mut());
435                self.apply_rules_to_dt(types, map.value_ty_mut());
436            }
437            DataType::Struct(s) => self.apply_rules_to_fields(types, &mut s.fields),
438            DataType::Enum(e) => {
439                for (_, variant) in &mut e.variants {
440                    self.apply_rules_to_fields(types, &mut variant.fields);
441                }
442            }
443            DataType::Tuple(tuple) => {
444                for dt in &mut tuple.elements {
445                    self.apply_rules_to_dt(types, dt);
446                }
447            }
448            DataType::Nullable(dt) => self.apply_rules_to_dt(types, dt),
449            DataType::Intersection(dts) => {
450                for dt in dts {
451                    self.apply_rules_to_dt(types, dt);
452                }
453            }
454            DataType::Reference(Reference::Named(reference)) => match &mut reference.inner {
455                NamedReferenceType::Recursive(_) | NamedReferenceType::Reference { .. } => {}
456                NamedReferenceType::Inline { dt, .. } => self.apply_rules_to_dt(types, dt),
457            },
458            DataType::Reference(Reference::Opaque(_)) => {}
459        }
460    }
461
462    fn apply_rules_to_fields(&self, types: &Types, fields: &mut Fields) {
463        match fields {
464            Fields::Unit => {}
465            Fields::Unnamed(fields) => {
466                for field in &mut fields.fields {
467                    if let Some(dt) = &mut field.ty {
468                        self.apply_rules_to_dt(types, dt);
469                    }
470                }
471            }
472            Fields::Named(fields) => {
473                for (_, field) in &mut fields.fields {
474                    if let Some(dt) = &mut field.ty {
475                        self.apply_rules_to_dt(types, dt);
476                    }
477                }
478            }
479        }
480    }
481
482    /// Scan a [`DataType`] tree applying serialize-facing rules.
483    ///
484    /// This assumes [`Configuration::apply_types`] has already been applied to the [`Types`].
485    /// Therefore the type updates will be shallow (up until references to the `Types`).
486    ///
487    /// The returned JavaScript expression is built around `js_ident` and may be
488    /// deeply nested for structs, tuples, lists, nullable values, and
489    /// intersections.
490    ///
491    /// If no rule or built-in remap matches, `None` is returned. If a rule
492    /// matches but the type shape does not need to change, `Some((None,
493    /// runtime_str))` is returned.
494    ///
495    pub fn apply_serialize(
496        &self,
497        types: &Types,
498        dt: &DataType,
499        js_ident: &str,
500    ) -> Option<(Option<DataType>, String)> {
501        self.apply_inner(
502            |rule| &rule.serialize,
503            serialize_bigint,
504            types,
505            dt,
506            js_ident,
507            &mut Vec::new(),
508        )
509    }
510
511    /// Scan a [`DataType`] tree applying deserialize-facing rules.
512    ///
513    /// Use this for values received from Rust before exposing them to
514    /// TypeScript callers.
515    pub fn apply_deserialize(
516        &self,
517        types: &Types,
518        dt: &DataType,
519        js_ident: &str,
520    ) -> Option<(Option<DataType>, String)> {
521        self.apply_inner(
522            |rule| &rule.deserialize,
523            deserialize_bigint,
524            types,
525            dt,
526            js_ident,
527            &mut Vec::new(),
528        )
529    }
530
531    fn apply_inner(
532        &self,
533        transform_for_rule: fn(&Rule) -> &Option<Transform>,
534        remap_bigint: fn() -> DataType,
535        types: &Types,
536        dt: &DataType,
537        js_ident: &str,
538        stack: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>,
539    ) -> Option<(Option<DataType>, String)> {
540        let result = match dt {
541            DataType::Reference(Reference::Named(r)) => {
542                if let Some(rule) = self.rule_for_reference(types, r) {
543                    return Some((
544                        Some((rule.data_type.0)(Self::reference_source_dt(types, r))),
545                        transform_for_rule(rule).as_ref().map_or_else(
546                            || js_ident.to_owned(),
547                            |transform| transform.apply(js_ident),
548                        ),
549                    ));
550                }
551
552                match &r.inner {
553                    NamedReferenceType::Inline { dt, .. } => self.apply_inner(
554                        transform_for_rule,
555                        remap_bigint,
556                        types,
557                        dt,
558                        js_ident,
559                        stack,
560                    ),
561                    NamedReferenceType::Recursive(_) => None,
562                    NamedReferenceType::Reference { .. } => {
563                        let ndt = types.get(r)?;
564
565                        let ty = ndt.ty.as_ref()?;
566                        let key = (ndt.name.clone(), ndt.module_path.clone());
567                        if stack.contains(&key) {
568                            return None;
569                        }
570                        stack.push(key);
571                        let result = self
572                            .apply_inner(
573                                transform_for_rule,
574                                remap_bigint,
575                                types,
576                                ty,
577                                js_ident,
578                                stack,
579                            )
580                            .map(|(_, runtime)| (None, runtime));
581                        stack.pop();
582                        result
583                    }
584                }
585            }
586            DataType::Struct(s) => match &s.fields {
587                Fields::Named(fields) => {
588                    let mut ty = s.clone();
589                    let mut changed = false;
590                    let mut parts = Vec::new();
591
592                    for (name, field) in &fields.fields {
593                        let Some(field_ty) = &field.ty else { continue };
594                        let field_ident = js_property_access(js_ident, name);
595                        let Some((next_ty, runtime)) = self.apply_inner(
596                            transform_for_rule,
597                            remap_bigint,
598                            types,
599                            field_ty,
600                            &field_ident,
601                            stack,
602                        ) else {
603                            continue;
604                        };
605
606                        if let Some(next_ty) = next_ty
607                            && let Fields::Named(fields) = &mut ty.fields
608                            && let Some((_, field)) =
609                                fields.fields.iter_mut().find(|(n, _)| n == name)
610                        {
611                            field.ty = Some(next_ty);
612                            changed = true;
613                        }
614                        if runtime != field_ident {
615                            parts.push(format!("{}:{runtime}", js_object_key(name)));
616                        }
617                    }
618
619                    if parts.is_empty() {
620                        changed.then_some((Some(DataType::Struct(ty)), js_ident.to_owned()))
621                    } else {
622                        Some((
623                            changed.then_some(DataType::Struct(ty)),
624                            spread_transform(js_ident, parts),
625                        ))
626                    }
627                }
628                Fields::Unnamed(fields) => {
629                    let mut ty = s.clone();
630                    let mut changed = false;
631                    let parts = fields
632                        .fields
633                        .iter()
634                        .enumerate()
635                        .filter_map(|(idx, field)| {
636                            let field_ty = field.ty.as_ref()?;
637                            let field_ident = format!("{js_ident}[{idx}]");
638                            let (next_ty, runtime) = self.apply_inner(
639                                transform_for_rule,
640                                remap_bigint,
641                                types,
642                                field_ty,
643                                &field_ident,
644                                stack,
645                            )?;
646
647                            if let Some(next_ty) = next_ty
648                                && let Fields::Unnamed(fields) = &mut ty.fields
649                            {
650                                fields.fields[idx].ty = Some(next_ty);
651                                changed = true;
652                            }
653
654                            (runtime != field_ident).then_some((idx, runtime))
655                        })
656                        .collect::<Vec<_>>();
657
658                    if parts.is_empty() {
659                        changed.then_some((Some(DataType::Struct(ty)), js_ident.to_owned()))
660                    } else {
661                        Some((
662                            changed.then_some(DataType::Struct(ty)),
663                            array_transform(js_ident, fields.fields.len(), parts),
664                        ))
665                    }
666                }
667                Fields::Unit => None,
668            },
669            DataType::Tuple(tuple) => {
670                let mut ty = tuple.clone();
671                let mut changed = false;
672                let parts = tuple
673                    .elements
674                    .iter()
675                    .enumerate()
676                    .filter_map(|(idx, element)| {
677                        let ident = format!("{js_ident}[{idx}]");
678                        let (next_ty, runtime) = self.apply_inner(
679                            transform_for_rule,
680                            remap_bigint,
681                            types,
682                            element,
683                            &ident,
684                            stack,
685                        )?;
686                        if let Some(next_ty) = next_ty {
687                            ty.elements[idx] = next_ty;
688                            changed = true;
689                        }
690                        (runtime != ident).then_some((idx, runtime))
691                    })
692                    .collect::<Vec<_>>();
693
694                if parts.is_empty() {
695                    changed.then_some((Some(DataType::Tuple(ty)), js_ident.to_owned()))
696                } else {
697                    Some((
698                        changed.then_some(DataType::Tuple(ty)),
699                        array_transform(js_ident, tuple.elements.len(), parts),
700                    ))
701                }
702            }
703            DataType::Map(map) => {
704                let item = "v";
705                let (next_ty, runtime) = self.apply_inner(
706                    transform_for_rule,
707                    remap_bigint,
708                    types,
709                    map.value_ty(),
710                    item,
711                    stack,
712                )?;
713
714                let mut ty = map.clone();
715                let mut changed = false;
716                if let Some(next_ty) = next_ty {
717                    ty.set_value_ty(next_ty);
718                    changed = true;
719                }
720
721                Some((
722                    changed.then_some(DataType::Map(ty)),
723                    format!(
724                        "Object.fromEntries(Object.entries({js_ident}).map(([k,{item}])=>[k,{runtime}]))"
725                    ),
726                ))
727            }
728            DataType::List(list) => {
729                let item = "i";
730                let (next_ty, runtime) = self.apply_inner(
731                    transform_for_rule,
732                    remap_bigint,
733                    types,
734                    &list.ty,
735                    item,
736                    stack,
737                )?;
738                let mut ty = list.clone();
739                let mut changed = false;
740                if let Some(next_ty) = next_ty {
741                    ty.ty = Box::new(next_ty);
742                    changed = true;
743                }
744                Some((
745                    changed.then_some(DataType::List(ty)),
746                    format!("{js_ident}.map({item}=>{runtime})"),
747                ))
748            }
749            DataType::Nullable(inner) => {
750                let (next_ty, runtime) = self.apply_inner(
751                    transform_for_rule,
752                    remap_bigint,
753                    types,
754                    inner,
755                    js_ident,
756                    stack,
757                )?;
758                Some((
759                    next_ty.map(|dt| DataType::Nullable(Box::new(dt))),
760                    format!("{js_ident}==null?{js_ident}:{runtime}"),
761                ))
762            }
763            DataType::Intersection(items) => {
764                let mut ty = items.clone();
765                let mut changed = false;
766                let parts = items
767                    .iter()
768                    .enumerate()
769                    .filter_map(|(idx, item)| {
770                        let (next_ty, runtime) = self.apply_inner(
771                            transform_for_rule,
772                            remap_bigint,
773                            types,
774                            item,
775                            js_ident,
776                            stack,
777                        )?;
778                        if let Some(next_ty) = next_ty {
779                            ty[idx] = next_ty;
780                            changed = true;
781                        }
782                        Some(runtime)
783                    })
784                    .collect::<Vec<_>>();
785
786                match parts.as_slice() {
787                    [] => None,
788                    [runtime] => Some((
789                        changed.then_some(DataType::Intersection(ty)),
790                        runtime.clone(),
791                    )),
792                    _ => Some((
793                        changed.then_some(DataType::Intersection(ty)),
794                        spread_transform(
795                            "",
796                            parts.into_iter().map(|p| format!("...{p}")).collect(),
797                        ),
798                    )),
799                }
800            }
801            DataType::Enum(_)
802            | DataType::Primitive(_)
803            | DataType::Generic(_)
804            | DataType::Reference(Reference::Opaque(_)) => None,
805        };
806
807        self.apply_builtin_remaps(remap_bigint, dt, js_ident, result)
808    }
809
810    fn rule_for_reference<'a>(
811        &'a self,
812        types: &'a Types,
813        reference: &specta::datatype::NamedReference,
814    ) -> Option<&'a Rule> {
815        let ndt = types.get(reference)?;
816        self.rules
817            .iter()
818            .find(|rule| rule.name == ndt.name && rule.module_path == ndt.module_path)
819    }
820
821    fn reference_source_dt(
822        types: &Types,
823        reference: &specta::datatype::NamedReference,
824    ) -> DataType {
825        match &reference.inner {
826            NamedReferenceType::Inline { dt, .. } => (**dt).clone(),
827            NamedReferenceType::Reference { .. } | NamedReferenceType::Recursive(_) => types
828                .get(reference)
829                .and_then(|ndt| ndt.ty.clone())
830                .unwrap_or_else(|| DataType::Reference(Reference::Named(reference.clone()))),
831        }
832    }
833
834    fn has_builtin_remaps(&self) -> bool {
835        self.lossless_bigint || self.lossless_floats
836    }
837
838    fn apply_builtin_remaps(
839        &self,
840        remap_bigint: fn() -> DataType,
841        dt: &DataType,
842        js_ident: &str,
843        result: Option<(Option<DataType>, String)>,
844    ) -> Option<(Option<DataType>, String)> {
845        if !self.has_builtin_remaps() {
846            return result;
847        }
848
849        let source = result
850            .as_ref()
851            .and_then(|(dt, _)| dt.clone())
852            .unwrap_or_else(|| dt.clone());
853        let mut remapped = source.clone();
854        apply_builtin_remaps(
855            &mut remapped,
856            remap_bigint,
857            self.lossless_bigint,
858            self.lossless_floats,
859        );
860
861        let runtime = result
862            .as_ref()
863            .map(|(_, runtime)| runtime.as_str())
864            .unwrap_or(js_ident);
865        let runtime = if is_lossless_bigint_primitive(&source)
866            && self.lossless_bigint
867            && remap_bigint() == deserialize_bigint()
868        {
869            format!("BigInt({runtime})")
870        } else {
871            runtime.to_owned()
872        };
873
874        if remapped == source {
875            result
876        } else {
877            Some((Some(remapped), runtime))
878        }
879    }
880}
881
882fn is_lossless_bigint_primitive(dt: &DataType) -> bool {
883    matches!(
884        dt,
885        DataType::Primitive(
886            Primitive::usize
887                | Primitive::isize
888                | Primitive::u64
889                | Primitive::i64
890                | Primitive::u128
891                | Primitive::i128
892        )
893    )
894}
895
896fn apply_builtin_remaps(
897    dt: &mut DataType,
898    remap_bigint: fn() -> DataType,
899    lossless_bigint: bool,
900    lossless_floats: bool,
901) {
902    if let DataType::Primitive(primitive) = dt
903        && let Some(remapped) = remap_primitive(
904            primitive.clone(),
905            remap_bigint,
906            lossless_bigint,
907            lossless_floats,
908        )
909    {
910        *dt = remapped;
911        return;
912    }
913
914    match dt {
915        DataType::Primitive(_) | DataType::Generic(_) => {}
916        DataType::List(list) => {
917            apply_builtin_remaps(&mut list.ty, remap_bigint, lossless_bigint, lossless_floats)
918        }
919        DataType::Map(map) => {
920            apply_builtin_remaps(
921                map.key_ty_mut(),
922                remap_bigint,
923                lossless_bigint,
924                lossless_floats,
925            );
926            apply_builtin_remaps(
927                map.value_ty_mut(),
928                remap_bigint,
929                lossless_bigint,
930                lossless_floats,
931            );
932        }
933        DataType::Struct(s) => apply_builtin_remaps_to_fields(
934            &mut s.fields,
935            remap_bigint,
936            lossless_bigint,
937            lossless_floats,
938        ),
939        DataType::Enum(e) => {
940            for (_, variant) in &mut e.variants {
941                apply_builtin_remaps_to_fields(
942                    &mut variant.fields,
943                    remap_bigint,
944                    lossless_bigint,
945                    lossless_floats,
946                );
947            }
948        }
949        DataType::Tuple(tuple) => {
950            for dt in &mut tuple.elements {
951                apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
952            }
953        }
954        DataType::Nullable(dt) => {
955            apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
956        }
957        DataType::Intersection(dts) => {
958            for dt in dts {
959                apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
960            }
961        }
962        DataType::Reference(reference) => {
963            let Reference::Named(reference) = reference else {
964                return;
965            };
966
967            match &mut reference.inner {
968                NamedReferenceType::Recursive(_) => {}
969                NamedReferenceType::Inline { dt, .. } => {
970                    apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
971                }
972                NamedReferenceType::Reference { generics, .. } => {
973                    for (_, dt) in generics {
974                        apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
975                    }
976                }
977            }
978        }
979    }
980}
981
982fn apply_builtin_remaps_to_fields(
983    fields: &mut Fields,
984    remap_bigint: fn() -> DataType,
985    lossless_bigint: bool,
986    lossless_floats: bool,
987) {
988    match fields {
989        Fields::Unit => {}
990        Fields::Unnamed(fields) => {
991            for field in &mut fields.fields {
992                if let Some(dt) = &mut field.ty {
993                    apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
994                }
995            }
996        }
997        Fields::Named(fields) => {
998            for (_, field) in &mut fields.fields {
999                if let Some(dt) = &mut field.ty {
1000                    apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
1001                }
1002            }
1003        }
1004    }
1005}
1006
1007fn remap_primitive(
1008    primitive: Primitive,
1009    remap_bigint: fn() -> DataType,
1010    lossless_bigint: bool,
1011    lossless_floats: bool,
1012) -> Option<DataType> {
1013    if lossless_bigint
1014        && matches!(
1015            primitive,
1016            Primitive::usize
1017                | Primitive::isize
1018                | Primitive::u64
1019                | Primitive::i64
1020                | Primitive::u128
1021                | Primitive::i128
1022        )
1023    {
1024        return Some(remap_bigint());
1025    }
1026
1027    if lossless_floats && matches!(primitive, Primitive::f16 | Primitive::f32 | Primitive::f64) {
1028        return Some(Reference::opaque(crate::opaque::Number).into());
1029    }
1030
1031    None
1032}
1033
1034fn serialize_bigint() -> DataType {
1035    crate::define("bigint | number").into()
1036}
1037
1038fn deserialize_bigint() -> DataType {
1039    Reference::opaque(crate::opaque::BigInt).into()
1040}
1041
1042fn spread_transform(js_ident: &str, mut parts: Vec<String>) -> String {
1043    if !js_ident.is_empty() {
1044        parts.insert(0, format!("...{js_ident}"));
1045    }
1046    format!("({{{}}})", parts.join(","))
1047}
1048
1049fn array_transform(js_ident: &str, len: usize, parts: Vec<(usize, String)>) -> String {
1050    let mut items = (0..len)
1051        .map(|idx| format!("{js_ident}[{idx}]"))
1052        .collect::<Vec<_>>();
1053
1054    for (idx, runtime) in parts {
1055        items[idx] = runtime;
1056    }
1057
1058    format!("([{}])", items.join(","))
1059}
1060
1061fn js_property_access(base: &str, name: &str) -> String {
1062    if is_identifier(name) {
1063        format!("{base}.{name}")
1064    } else {
1065        format!("{base}[\"{}\"]", escape_typescript_string_literal(name))
1066    }
1067}
1068
1069fn js_object_key(name: &str) -> Cow<'_, str> {
1070    if is_identifier(name) {
1071        Cow::Borrowed(name)
1072    } else {
1073        Cow::Owned(format!("\"{}\"", escape_typescript_string_literal(name)))
1074    }
1075}