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 a registered graph contains no runtime transform, `None` is returned;
492    /// its type-only changes are handled by [`Configuration::apply_types`]. For
493    /// inline inputs, a type-only result is retained with an identity runtime
494    /// expression because there is no registered type for `apply_types` to rewrite.
495    ///
496    pub fn apply_serialize(
497        &self,
498        types: &Types,
499        dt: &DataType,
500        js_ident: &str,
501    ) -> Option<(Option<DataType>, String)> {
502        let result = self.apply_inner(
503            |rule| &rule.serialize,
504            serialize_bigint,
505            types,
506            dt,
507            js_ident,
508            &mut Vec::new(),
509        );
510        prune_identity_root(types, dt, js_ident, result)
511    }
512
513    /// Scan a [`DataType`] tree applying deserialize-facing rules.
514    ///
515    /// Use this for values received from Rust before exposing them to
516    /// TypeScript callers.
517    pub fn apply_deserialize(
518        &self,
519        types: &Types,
520        dt: &DataType,
521        js_ident: &str,
522    ) -> Option<(Option<DataType>, String)> {
523        let result = self.apply_inner(
524            |rule| &rule.deserialize,
525            deserialize_bigint,
526            types,
527            dt,
528            js_ident,
529            &mut Vec::new(),
530        );
531        prune_identity_root(types, dt, js_ident, result)
532    }
533
534    fn apply_inner(
535        &self,
536        transform_for_rule: fn(&Rule) -> &Option<Transform>,
537        remap_bigint: fn() -> DataType,
538        types: &Types,
539        dt: &DataType,
540        js_ident: &str,
541        stack: &mut Vec<(Cow<'static, str>, Cow<'static, str>)>,
542    ) -> Option<(Option<DataType>, String)> {
543        let result = match dt {
544            DataType::Reference(Reference::Named(r)) => {
545                if let Some(rule) = self.rule_for_reference(types, r) {
546                    return Some((
547                        Some((rule.data_type.0)(Self::reference_source_dt(types, r))),
548                        transform_for_rule(rule).as_ref().map_or_else(
549                            || js_ident.to_owned(),
550                            |transform| transform.apply(js_ident),
551                        ),
552                    ));
553                }
554
555                match &r.inner {
556                    NamedReferenceType::Inline { dt, .. } => self.apply_inner(
557                        transform_for_rule,
558                        remap_bigint,
559                        types,
560                        dt,
561                        js_ident,
562                        stack,
563                    ),
564                    NamedReferenceType::Recursive(_) => None,
565                    NamedReferenceType::Reference { .. } => {
566                        let ndt = types.get(r)?;
567
568                        let ty = ndt.ty.as_ref()?;
569                        let key = (ndt.name.clone(), ndt.module_path.clone());
570                        if stack.contains(&key) {
571                            return None;
572                        }
573                        stack.push(key);
574                        let result = self
575                            .apply_inner(
576                                transform_for_rule,
577                                remap_bigint,
578                                types,
579                                ty,
580                                js_ident,
581                                stack,
582                            )
583                            .map(|(_, runtime)| (None, runtime));
584                        stack.pop();
585                        result
586                    }
587                }
588            }
589            DataType::Struct(s) => match &s.fields {
590                Fields::Named(fields) => {
591                    let mut ty = s.clone();
592                    let mut changed = false;
593                    let mut parts = Vec::new();
594
595                    for (name, field) in &fields.fields {
596                        let Some(field_ty) = &field.ty else { continue };
597                        let field_ident = js_property_access(js_ident, name);
598                        let Some((next_ty, runtime)) = self.apply_inner(
599                            transform_for_rule,
600                            remap_bigint,
601                            types,
602                            field_ty,
603                            &field_ident,
604                            stack,
605                        ) else {
606                            continue;
607                        };
608
609                        if let Some(next_ty) = next_ty
610                            && let Fields::Named(fields) = &mut ty.fields
611                            && let Some((_, field)) =
612                                fields.fields.iter_mut().find(|(n, _)| n == name)
613                        {
614                            field.ty = Some(next_ty);
615                            changed = true;
616                        }
617                        if runtime != field_ident {
618                            parts.push(format!("{}:{runtime}", js_object_key(name)));
619                        }
620                    }
621
622                    if parts.is_empty() {
623                        changed.then_some((Some(DataType::Struct(ty)), js_ident.to_owned()))
624                    } else {
625                        Some((
626                            changed.then_some(DataType::Struct(ty)),
627                            spread_transform(js_ident, parts),
628                        ))
629                    }
630                }
631                Fields::Unnamed(fields) => {
632                    let mut ty = s.clone();
633                    let mut changed = false;
634                    let is_newtype = fields.fields.len() == 1;
635                    let parts = fields
636                        .fields
637                        .iter()
638                        .enumerate()
639                        .filter_map(|(idx, field)| {
640                            let field_ty = field.ty.as_ref()?;
641                            let field_ident = if is_newtype {
642                                js_ident.to_owned()
643                            } else {
644                                format!("{js_ident}[{idx}]")
645                            };
646                            let (next_ty, runtime) = self.apply_inner(
647                                transform_for_rule,
648                                remap_bigint,
649                                types,
650                                field_ty,
651                                &field_ident,
652                                stack,
653                            )?;
654
655                            if let Some(next_ty) = next_ty
656                                && let Fields::Unnamed(fields) = &mut ty.fields
657                            {
658                                fields.fields[idx].ty = Some(next_ty);
659                                changed = true;
660                            }
661
662                            (runtime != field_ident).then_some((idx, runtime))
663                        })
664                        .collect::<Vec<_>>();
665
666                    if parts.is_empty() {
667                        changed.then_some((Some(DataType::Struct(ty)), js_ident.to_owned()))
668                    } else {
669                        Some((
670                            changed.then_some(DataType::Struct(ty)),
671                            if is_newtype {
672                                parts.into_iter().next()?.1
673                            } else {
674                                array_transform(js_ident, fields.fields.len(), parts)
675                            },
676                        ))
677                    }
678                }
679                Fields::Unit => None,
680            },
681            DataType::Tuple(tuple) => {
682                let mut ty = tuple.clone();
683                let mut changed = false;
684                let parts = tuple
685                    .elements
686                    .iter()
687                    .enumerate()
688                    .filter_map(|(idx, element)| {
689                        let ident = format!("{js_ident}[{idx}]");
690                        let (next_ty, runtime) = self.apply_inner(
691                            transform_for_rule,
692                            remap_bigint,
693                            types,
694                            element,
695                            &ident,
696                            stack,
697                        )?;
698                        if let Some(next_ty) = next_ty {
699                            ty.elements[idx] = next_ty;
700                            changed = true;
701                        }
702                        (runtime != ident).then_some((idx, runtime))
703                    })
704                    .collect::<Vec<_>>();
705
706                if parts.is_empty() {
707                    changed.then_some((Some(DataType::Tuple(ty)), js_ident.to_owned()))
708                } else {
709                    Some((
710                        changed.then_some(DataType::Tuple(ty)),
711                        array_transform(js_ident, tuple.elements.len(), parts),
712                    ))
713                }
714            }
715            DataType::Map(map) => {
716                let item = "v";
717                let (next_ty, runtime) = self.apply_inner(
718                    transform_for_rule,
719                    remap_bigint,
720                    types,
721                    map.value_ty(),
722                    item,
723                    stack,
724                )?;
725
726                let mut ty = map.clone();
727                let mut changed = false;
728                if let Some(next_ty) = next_ty {
729                    ty.set_value_ty(next_ty);
730                    changed = true;
731                }
732
733                Some((
734                    changed.then_some(DataType::Map(ty)),
735                    if runtime == item {
736                        js_ident.to_owned()
737                    } else {
738                        format!(
739                            "Object.fromEntries(Object.entries({js_ident}).map(([k,{item}])=>[k,{runtime}]))"
740                        )
741                    },
742                ))
743            }
744            DataType::List(list) => {
745                let item = "i";
746                let (next_ty, runtime) = self.apply_inner(
747                    transform_for_rule,
748                    remap_bigint,
749                    types,
750                    &list.ty,
751                    item,
752                    stack,
753                )?;
754                let mut ty = list.clone();
755                let mut changed = false;
756                if let Some(next_ty) = next_ty {
757                    ty.ty = Box::new(next_ty);
758                    changed = true;
759                }
760                Some((
761                    changed.then_some(DataType::List(ty)),
762                    if runtime == item {
763                        js_ident.to_owned()
764                    } else {
765                        format!("{js_ident}.map({item}=>{runtime})")
766                    },
767                ))
768            }
769            DataType::Nullable(inner) => {
770                let (next_ty, runtime) = self.apply_inner(
771                    transform_for_rule,
772                    remap_bigint,
773                    types,
774                    inner,
775                    js_ident,
776                    stack,
777                )?;
778                Some((
779                    next_ty.map(|dt| DataType::Nullable(Box::new(dt))),
780                    if runtime == js_ident {
781                        js_ident.to_owned()
782                    } else {
783                        format!("{js_ident}==null?{js_ident}:{runtime}")
784                    },
785                ))
786            }
787            DataType::Intersection(items) => {
788                let mut ty = items.clone();
789                let mut changed = false;
790                let mut parts = items
791                    .iter()
792                    .enumerate()
793                    .filter_map(|(idx, item)| {
794                        let (next_ty, runtime) = self.apply_inner(
795                            transform_for_rule,
796                            remap_bigint,
797                            types,
798                            item,
799                            js_ident,
800                            stack,
801                        )?;
802                        if let Some(next_ty) = next_ty {
803                            ty[idx] = next_ty;
804                            changed = true;
805                        }
806                        Some(runtime)
807                    })
808                    .collect::<Vec<_>>();
809
810                parts.dedup();
811
812                match parts.as_slice() {
813                    [] => None,
814                    [runtime] => Some((
815                        changed.then_some(DataType::Intersection(ty)),
816                        runtime.clone(),
817                    )),
818                    _ => Some((
819                        changed.then_some(DataType::Intersection(ty)),
820                        spread_transform(
821                            "",
822                            parts.into_iter().map(|p| format!("...{p}")).collect(),
823                        ),
824                    )),
825                }
826            }
827            DataType::Enum(_)
828            | DataType::Primitive(_)
829            | DataType::Generic(_)
830            | DataType::Reference(Reference::Opaque(_)) => None,
831        };
832
833        self.apply_builtin_remaps(remap_bigint, dt, js_ident, result)
834    }
835
836    fn rule_for_reference<'a>(
837        &'a self,
838        types: &'a Types,
839        reference: &specta::datatype::NamedReference,
840    ) -> Option<&'a Rule> {
841        let ndt = types.get(reference)?;
842        self.rules
843            .iter()
844            .find(|rule| rule.name == ndt.name && rule.module_path == ndt.module_path)
845    }
846
847    fn reference_source_dt(
848        types: &Types,
849        reference: &specta::datatype::NamedReference,
850    ) -> DataType {
851        match &reference.inner {
852            NamedReferenceType::Inline { dt, .. } => (**dt).clone(),
853            NamedReferenceType::Reference { .. } | NamedReferenceType::Recursive(_) => types
854                .get(reference)
855                .and_then(|ndt| ndt.ty.clone())
856                .unwrap_or_else(|| DataType::Reference(Reference::Named(reference.clone()))),
857        }
858    }
859
860    fn has_builtin_remaps(&self) -> bool {
861        self.lossless_bigint || self.lossless_floats
862    }
863
864    fn apply_builtin_remaps(
865        &self,
866        remap_bigint: fn() -> DataType,
867        dt: &DataType,
868        js_ident: &str,
869        result: Option<(Option<DataType>, String)>,
870    ) -> Option<(Option<DataType>, String)> {
871        if !self.has_builtin_remaps() {
872            return result;
873        }
874
875        let source = result
876            .as_ref()
877            .and_then(|(dt, _)| dt.clone())
878            .unwrap_or_else(|| dt.clone());
879        let mut remapped = source.clone();
880        apply_builtin_remaps(
881            &mut remapped,
882            remap_bigint,
883            self.lossless_bigint,
884            self.lossless_floats,
885        );
886
887        let runtime = result
888            .as_ref()
889            .map(|(_, runtime)| runtime.as_str())
890            .unwrap_or(js_ident);
891        let runtime = if is_lossless_bigint_primitive(&source)
892            && self.lossless_bigint
893            && remap_bigint() == deserialize_bigint()
894        {
895            format!("BigInt({runtime})")
896        } else {
897            runtime.to_owned()
898        };
899
900        if remapped == source {
901            result
902        } else {
903            Some((Some(remapped), runtime))
904        }
905    }
906}
907
908fn prune_identity_root(
909    types: &Types,
910    dt: &DataType,
911    js_ident: &str,
912    result: Option<(Option<DataType>, String)>,
913) -> Option<(Option<DataType>, String)> {
914    let registered_root = matches!(
915        dt,
916        DataType::Reference(Reference::Named(reference))
917            if !matches!(reference.inner, NamedReferenceType::Inline { .. })
918                && types.get(reference).is_some_and(|ndt| ndt.ty.is_some())
919    );
920
921    result.filter(|(next_ty, runtime)| {
922        runtime != js_ident
923            || next_ty
924                .as_ref()
925                .is_some_and(|next_ty| !registered_root || registered_generics_changed(dt, next_ty))
926    })
927}
928
929fn registered_generics_changed(dt: &DataType, next_ty: &DataType) -> bool {
930    let (
931        DataType::Reference(Reference::Named(source)),
932        DataType::Reference(Reference::Named(remapped)),
933    ) = (dt, next_ty)
934    else {
935        return false;
936    };
937    let (
938        NamedReferenceType::Reference {
939            generics: source, ..
940        },
941        NamedReferenceType::Reference {
942            generics: remapped, ..
943        },
944    ) = (&source.inner, &remapped.inner)
945    else {
946        return false;
947    };
948
949    source != remapped
950}
951
952fn is_lossless_bigint_primitive(dt: &DataType) -> bool {
953    matches!(
954        dt,
955        DataType::Primitive(
956            Primitive::usize
957                | Primitive::isize
958                | Primitive::u64
959                | Primitive::i64
960                | Primitive::u128
961                | Primitive::i128
962        )
963    )
964}
965
966fn apply_builtin_remaps(
967    dt: &mut DataType,
968    remap_bigint: fn() -> DataType,
969    lossless_bigint: bool,
970    lossless_floats: bool,
971) {
972    if let DataType::Primitive(primitive) = dt
973        && let Some(remapped) = remap_primitive(
974            primitive.clone(),
975            remap_bigint,
976            lossless_bigint,
977            lossless_floats,
978        )
979    {
980        *dt = remapped;
981        return;
982    }
983
984    match dt {
985        DataType::Primitive(_) | DataType::Generic(_) => {}
986        DataType::List(list) => {
987            apply_builtin_remaps(&mut list.ty, remap_bigint, lossless_bigint, lossless_floats)
988        }
989        DataType::Map(map) => {
990            apply_builtin_remaps(
991                map.key_ty_mut(),
992                remap_bigint,
993                lossless_bigint,
994                lossless_floats,
995            );
996            apply_builtin_remaps(
997                map.value_ty_mut(),
998                remap_bigint,
999                lossless_bigint,
1000                lossless_floats,
1001            );
1002        }
1003        DataType::Struct(s) => apply_builtin_remaps_to_fields(
1004            &mut s.fields,
1005            remap_bigint,
1006            lossless_bigint,
1007            lossless_floats,
1008        ),
1009        DataType::Enum(e) => {
1010            for (_, variant) in &mut e.variants {
1011                apply_builtin_remaps_to_fields(
1012                    &mut variant.fields,
1013                    remap_bigint,
1014                    lossless_bigint,
1015                    lossless_floats,
1016                );
1017            }
1018        }
1019        DataType::Tuple(tuple) => {
1020            for dt in &mut tuple.elements {
1021                apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
1022            }
1023        }
1024        DataType::Nullable(dt) => {
1025            apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
1026        }
1027        DataType::Intersection(dts) => {
1028            for dt in dts {
1029                apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
1030            }
1031        }
1032        DataType::Reference(reference) => {
1033            let Reference::Named(reference) = reference else {
1034                return;
1035            };
1036
1037            match &mut reference.inner {
1038                NamedReferenceType::Recursive(_) => {}
1039                NamedReferenceType::Inline { dt, .. } => {
1040                    apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
1041                }
1042                NamedReferenceType::Reference { generics, .. } => {
1043                    for (_, dt) in generics {
1044                        apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
1045                    }
1046                }
1047            }
1048        }
1049    }
1050}
1051
1052fn apply_builtin_remaps_to_fields(
1053    fields: &mut Fields,
1054    remap_bigint: fn() -> DataType,
1055    lossless_bigint: bool,
1056    lossless_floats: bool,
1057) {
1058    match fields {
1059        Fields::Unit => {}
1060        Fields::Unnamed(fields) => {
1061            for field in &mut fields.fields {
1062                if let Some(dt) = &mut field.ty {
1063                    apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
1064                }
1065            }
1066        }
1067        Fields::Named(fields) => {
1068            for (_, field) in &mut fields.fields {
1069                if let Some(dt) = &mut field.ty {
1070                    apply_builtin_remaps(dt, remap_bigint, lossless_bigint, lossless_floats);
1071                }
1072            }
1073        }
1074    }
1075}
1076
1077fn remap_primitive(
1078    primitive: Primitive,
1079    remap_bigint: fn() -> DataType,
1080    lossless_bigint: bool,
1081    lossless_floats: bool,
1082) -> Option<DataType> {
1083    if lossless_bigint
1084        && matches!(
1085            primitive,
1086            Primitive::usize
1087                | Primitive::isize
1088                | Primitive::u64
1089                | Primitive::i64
1090                | Primitive::u128
1091                | Primitive::i128
1092        )
1093    {
1094        return Some(remap_bigint());
1095    }
1096
1097    if lossless_floats && matches!(primitive, Primitive::f16 | Primitive::f32 | Primitive::f64) {
1098        return Some(Reference::opaque(crate::opaque::Number).into());
1099    }
1100
1101    None
1102}
1103
1104fn serialize_bigint() -> DataType {
1105    crate::define("bigint | number").into()
1106}
1107
1108fn deserialize_bigint() -> DataType {
1109    Reference::opaque(crate::opaque::BigInt).into()
1110}
1111
1112fn spread_transform(js_ident: &str, mut parts: Vec<String>) -> String {
1113    if !js_ident.is_empty() {
1114        parts.insert(0, format!("...{js_ident}"));
1115    }
1116    format!("({{{}}})", parts.join(","))
1117}
1118
1119fn array_transform(js_ident: &str, len: usize, parts: Vec<(usize, String)>) -> String {
1120    let mut items = (0..len)
1121        .map(|idx| format!("{js_ident}[{idx}]"))
1122        .collect::<Vec<_>>();
1123
1124    for (idx, runtime) in parts {
1125        items[idx] = runtime;
1126    }
1127
1128    format!("([{}])", items.join(","))
1129}
1130
1131fn js_property_access(base: &str, name: &str) -> String {
1132    if is_identifier(name) {
1133        format!("{base}.{name}")
1134    } else {
1135        format!("{base}[\"{}\"]", escape_typescript_string_literal(name))
1136    }
1137}
1138
1139fn js_object_key(name: &str) -> Cow<'_, str> {
1140    if is_identifier(name) {
1141        Cow::Borrowed(name)
1142    } else {
1143        Cow::Owned(format!("\"{}\"", escape_typescript_string_literal(name)))
1144    }
1145}