specta_typescript/
lib.rs

1//! [TypeScript](https://www.typescriptlang.org) language exporter.
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![doc(
4    html_logo_url = "https://github.com/oscartbeaumont/specta/raw/main/.github/logo-128.png",
5    html_favicon_url = "https://github.com/oscartbeaumont/specta/raw/main/.github/logo-128.png"
6)]
7
8use std::borrow::Cow;
9use std::fmt::Write;
10
11pub mod comments;
12mod context;
13mod error;
14pub mod formatter;
15pub mod js_doc;
16mod reserved_terms;
17mod typescript;
18
19pub use context::*;
20pub use error::*;
21use reserved_terms::*;
22pub use typescript::*;
23
24use specta::datatype::{
25    DataType, DeprecatedType, EnumRepr, EnumType, EnumVariant, EnumVariants, FunctionResultVariant,
26    LiteralType, NamedDataType, PrimitiveType, StructFields, StructType, TupleType,
27};
28use specta::{
29    internal::{detect_duplicate_type_names, skip_fields, skip_fields_named, NonSkipField},
30    Generics, NamedType, Type, TypeCollection,
31};
32use specta_serde::is_valid_ty;
33
34#[allow(missing_docs)]
35pub type Result<T> = std::result::Result<T, ExportError>;
36
37pub(crate) type Output = Result<String>;
38
39/// Convert a type which implements [`Type`] to a TypeScript string with an export.
40///
41/// Eg. `export type Foo = { demo: string; };`
42pub fn export_ref<T: NamedType>(_: &T, conf: &Typescript) -> Output {
43    export::<T>(conf)
44}
45
46/// Convert a type which implements [`Type`] to a TypeScript string with an export.
47///
48/// Eg. `export type Foo = { demo: string; };`
49pub fn export<T: NamedType>(conf: &Typescript) -> Output {
50    let mut type_map = TypeCollection::default();
51    let named_data_type = T::definition_named_data_type(&mut type_map);
52    is_valid_ty(&named_data_type.inner, &type_map)?;
53    let result = export_named_datatype(conf, &named_data_type, &type_map);
54
55    if let Some((ty_name, l0, l1)) = detect_duplicate_type_names(&type_map).into_iter().next() {
56        return Err(ExportError::DuplicateTypeName(ty_name, l0, l1));
57    }
58
59    result
60}
61
62/// Convert a type which implements [`Type`] to a TypeScript string.
63///
64/// Eg. `{ demo: string; };`
65pub fn inline_ref<T: Type>(_: &T, conf: &Typescript) -> Output {
66    inline::<T>(conf)
67}
68
69/// Convert a type which implements [`Type`] to a TypeScript string.
70///
71/// Eg. `{ demo: string; };`
72pub fn inline<T: Type>(conf: &Typescript) -> Output {
73    let mut type_map = TypeCollection::default();
74    let ty = T::inline(&mut type_map, Generics::NONE);
75    is_valid_ty(&ty, &type_map)?;
76    let result = datatype(conf, &FunctionResultVariant::Value(ty.clone()), &type_map);
77
78    if let Some((ty_name, l0, l1)) = detect_duplicate_type_names(&type_map).into_iter().next() {
79        return Err(ExportError::DuplicateTypeName(ty_name, l0, l1));
80    }
81
82    result
83}
84
85/// Convert a DataType to a TypeScript string
86///
87/// Eg. `export Name = { demo: string; }`
88pub fn export_named_datatype(
89    conf: &Typescript,
90    typ: &NamedDataType,
91    type_map: &TypeCollection,
92) -> Output {
93    // TODO: Duplicate type name detection?
94
95    is_valid_ty(&typ.inner, type_map)?;
96    export_datatype_inner(
97        ExportContext {
98            cfg: conf,
99            path: vec![],
100            is_export: true,
101        },
102        typ,
103        type_map,
104    )
105}
106
107#[allow(clippy::ptr_arg)]
108fn inner_comments(
109    ctx: ExportContext,
110    deprecated: Option<&DeprecatedType>,
111    docs: &Cow<'static, str>,
112    other: String,
113    start_with_newline: bool,
114) -> String {
115    if !ctx.is_export {
116        return other;
117    }
118
119    let comments = ctx
120        .cfg
121        .comment_exporter
122        .map(|v| v(CommentFormatterArgs { docs, deprecated }))
123        .unwrap_or_default();
124
125    let prefix = match start_with_newline && !comments.is_empty() {
126        true => "\n",
127        false => "",
128    };
129
130    format!("{prefix}{comments}{other}")
131}
132
133fn export_datatype_inner(
134    ctx: ExportContext,
135    typ: &NamedDataType,
136    type_map: &TypeCollection,
137) -> Output {
138    let name = typ.name();
139    let docs = typ.docs();
140    let ext = typ.ext();
141    let deprecated = typ.deprecated();
142    let item = &typ.inner;
143
144    let ctx = ctx.with(
145        ext.clone()
146            .map(|v| PathItem::TypeExtended(name.clone(), *v.impl_location()))
147            .unwrap_or_else(|| PathItem::Type(name.clone())),
148    );
149    let name = sanitise_type_name(ctx.clone(), NamedLocation::Type, name)?;
150
151    let generics = item
152        .generics()
153        .filter(|generics| !generics.is_empty())
154        .map(|generics| format!("<{}>", generics.join(", ")))
155        .unwrap_or_default();
156
157    let mut inline_ts = String::new();
158    datatype_inner(
159        ctx.clone(),
160        &FunctionResultVariant::Value((typ.inner).clone()),
161        type_map,
162        &mut inline_ts,
163    )?;
164
165    Ok(inner_comments(
166        ctx,
167        deprecated,
168        docs,
169        format!("export type {name}{generics} = {inline_ts}"),
170        false,
171    ))
172}
173
174/// Convert a DataType to a TypeScript string
175///
176/// Eg. `{ demo: string; }`
177pub fn datatype(
178    conf: &Typescript,
179    typ: &FunctionResultVariant,
180    type_map: &TypeCollection,
181) -> Output {
182    // TODO: Duplicate type name detection?
183
184    let mut s = String::new();
185    datatype_inner(
186        ExportContext {
187            cfg: conf,
188            path: vec![],
189            is_export: false,
190        },
191        typ,
192        type_map,
193        &mut s,
194    )
195    .map(|_| s)
196}
197
198macro_rules! primitive_def {
199    ($($t:ident)+) => {
200        $(PrimitiveType::$t)|+
201    }
202}
203
204pub(crate) fn datatype_inner(
205    ctx: ExportContext,
206    typ: &FunctionResultVariant,
207    type_map: &TypeCollection,
208    s: &mut String,
209) -> Result<()> {
210    let typ = match typ {
211        FunctionResultVariant::Value(t) => t,
212        FunctionResultVariant::Result(t, e) => {
213            let mut variants = vec![
214                {
215                    let mut v = String::new();
216                    datatype_inner(
217                        ctx.clone(),
218                        &FunctionResultVariant::Value(t.clone()),
219                        type_map,
220                        &mut v,
221                    )?;
222                    v
223                },
224                {
225                    let mut v = String::new();
226                    datatype_inner(
227                        ctx,
228                        &FunctionResultVariant::Value(e.clone()),
229                        type_map,
230                        &mut v,
231                    )?;
232                    v
233                },
234            ];
235            variants.dedup();
236            s.push_str(&variants.join(" | "));
237            return Ok(());
238        }
239    };
240
241    Ok(match &typ {
242        DataType::Any => s.push_str(ANY),
243        DataType::Unknown => s.push_str(UNKNOWN),
244        DataType::Primitive(p) => {
245            let ctx = ctx.with(PathItem::Type(p.to_rust_str().into()));
246            let str = match p {
247                primitive_def!(i8 i16 i32 u8 u16 u32 f32 f64) => NUMBER,
248                primitive_def!(usize isize i64 u64 i128 u128) => match ctx.cfg.bigint {
249                    BigIntExportBehavior::String => STRING,
250                    BigIntExportBehavior::Number => NUMBER,
251                    BigIntExportBehavior::BigInt => BIGINT,
252                    BigIntExportBehavior::Fail => {
253                        return Err(ExportError::BigIntForbidden(ctx.export_path()));
254                    }
255                    BigIntExportBehavior::FailWithReason(reason) => {
256                        return Err(ExportError::Other(ctx.export_path(), reason.to_owned()))
257                    }
258                },
259                primitive_def!(String char) => STRING,
260                primitive_def!(bool) => BOOLEAN,
261            };
262
263            s.push_str(str);
264        }
265        DataType::Literal(literal) => match literal {
266            LiteralType::i8(v) => write!(s, "{v}")?,
267            LiteralType::i16(v) => write!(s, "{v}")?,
268            LiteralType::i32(v) => write!(s, "{v}")?,
269            LiteralType::u8(v) => write!(s, "{v}")?,
270            LiteralType::u16(v) => write!(s, "{v}")?,
271            LiteralType::u32(v) => write!(s, "{v}")?,
272            LiteralType::f32(v) => write!(s, "{v}")?,
273            LiteralType::f64(v) => write!(s, "{v}")?,
274            LiteralType::bool(v) => write!(s, "{v}")?,
275            LiteralType::String(v) => write!(s, r#""{v}""#)?,
276            LiteralType::char(v) => write!(s, r#""{v}""#)?,
277            LiteralType::None => s.write_str(NULL)?,
278            _ => unreachable!(),
279        },
280        DataType::Nullable(def) => {
281            datatype_inner(
282                ctx,
283                &FunctionResultVariant::Value((**def).clone()),
284                type_map,
285                s,
286            )?;
287
288            let or_null = format!(" | {NULL}");
289            if !s.ends_with(&or_null) {
290                s.push_str(&or_null);
291            }
292        }
293        DataType::Map(def) => {
294            // We use `{ [key in K]: V }` instead of `Record<K, V>` to avoid issues with circular references.
295            // Wrapped in Partial<> because otherwise TypeScript would enforce exhaustiveness.
296            s.push_str("Partial<{ [key in ");
297            datatype_inner(
298                ctx.clone(),
299                &FunctionResultVariant::Value(def.key_ty().clone()),
300                type_map,
301                s,
302            )?;
303            s.push_str("]: ");
304            datatype_inner(
305                ctx.clone(),
306                &FunctionResultVariant::Value(def.value_ty().clone()),
307                type_map,
308                s,
309            )?;
310            s.push_str(" }>");
311        }
312        // We use `T[]` instead of `Array<T>` to avoid issues with circular references.
313        DataType::List(def) => {
314            let mut dt = String::new();
315            datatype_inner(
316                ctx,
317                &FunctionResultVariant::Value(def.ty().clone()),
318                type_map,
319                &mut dt,
320            )?;
321
322            let dt = if (dt.contains(' ') && !dt.ends_with('}'))
323                // This is to do with maintaining order of operations.
324                // Eg `{} | {}` must be wrapped in parens like `({} | {})[]` but `{}` doesn't cause `{}[]` is valid
325                || (dt.contains(' ') && (dt.contains('&') || dt.contains('|')))
326            {
327                format!("({dt})")
328            } else {
329                dt
330            };
331
332            if let Some(length) = def.length() {
333                s.push('[');
334
335                for n in 0..length {
336                    if n != 0 {
337                        s.push_str(", ");
338                    }
339
340                    s.push_str(&dt);
341                }
342
343                s.push(']');
344            } else {
345                write!(s, "{dt}[]")?;
346            }
347        }
348        DataType::Struct(item) => struct_datatype(
349            ctx.with(
350                item.sid()
351                    .and_then(|sid| type_map.get(*sid))
352                    .and_then(|v| v.ext())
353                    .map(|v| PathItem::TypeExtended(item.name().clone(), *v.impl_location()))
354                    .unwrap_or_else(|| PathItem::Type(item.name().clone())),
355            ),
356            item.name(),
357            item,
358            type_map,
359            s,
360        )?,
361        DataType::Enum(item) => {
362            let mut ctx = ctx.clone();
363            let cfg = ctx.cfg.clone().bigint(BigIntExportBehavior::Number);
364            if item.skip_bigint_checks() {
365                ctx.cfg = &cfg;
366            }
367
368            enum_datatype(
369                ctx.with(PathItem::Variant(item.name().clone())),
370                item,
371                type_map,
372                s,
373            )?
374        }
375        DataType::Tuple(tuple) => s.push_str(&tuple_datatype(ctx, tuple, type_map)?),
376        DataType::Reference(reference) => match &reference.generics()[..] {
377            [] => s.push_str(&reference.name()),
378            generics => {
379                s.push_str(&reference.name());
380                s.push('<');
381
382                for (i, (_, v)) in generics.iter().enumerate() {
383                    if i != 0 {
384                        s.push_str(", ");
385                    }
386
387                    datatype_inner(
388                        ctx.with(PathItem::Type(reference.name().clone())),
389                        &FunctionResultVariant::Value(v.clone()),
390                        type_map,
391                        s,
392                    )?;
393                }
394
395                s.push('>');
396            }
397        },
398        DataType::Generic(ident) => s.push_str(&ident.to_string()),
399    })
400}
401
402// Can be used with `StructUnnamedFields.fields` or `EnumNamedFields.fields`
403fn unnamed_fields_datatype(
404    ctx: ExportContext,
405    fields: &[NonSkipField],
406    type_map: &TypeCollection,
407    s: &mut String,
408) -> Result<()> {
409    Ok(match fields {
410        [(field, ty)] => {
411            let mut v = String::new();
412            datatype_inner(
413                ctx.clone(),
414                &FunctionResultVariant::Value((*ty).clone()),
415                type_map,
416                &mut v,
417            )?;
418            s.push_str(&inner_comments(
419                ctx,
420                field.deprecated(),
421                field.docs(),
422                v,
423                true,
424            ));
425        }
426        fields => {
427            s.push('[');
428
429            for (i, (field, ty)) in fields.iter().enumerate() {
430                if i != 0 {
431                    s.push_str(", ");
432                }
433
434                let mut v = String::new();
435                datatype_inner(
436                    ctx.clone(),
437                    &FunctionResultVariant::Value((*ty).clone()),
438                    type_map,
439                    &mut v,
440                )?;
441                s.push_str(&inner_comments(
442                    ctx.clone(),
443                    field.deprecated(),
444                    field.docs(),
445                    v,
446                    true,
447                ));
448            }
449
450            s.push(']');
451        }
452    })
453}
454
455fn tuple_datatype(ctx: ExportContext, tuple: &TupleType, type_map: &TypeCollection) -> Output {
456    match &tuple.elements()[..] {
457        [] => Ok(NULL.to_string()),
458        tys => Ok(format!(
459            "[{}]",
460            tys.iter()
461                .map(|v| {
462                    let mut s = String::new();
463                    datatype_inner(
464                        ctx.clone(),
465                        &FunctionResultVariant::Value(v.clone()),
466                        type_map,
467                        &mut s,
468                    )
469                    .map(|_| s)
470                })
471                .collect::<Result<Vec<_>>>()?
472                .join(", ")
473        )),
474    }
475}
476
477fn struct_datatype(
478    ctx: ExportContext,
479    key: &str,
480    strct: &StructType,
481    type_map: &TypeCollection,
482    s: &mut String,
483) -> Result<()> {
484    Ok(match &strct.fields() {
485        StructFields::Unit => s.push_str(NULL),
486        StructFields::Unnamed(unnamed) => unnamed_fields_datatype(
487            ctx,
488            &skip_fields(unnamed.fields()).collect::<Vec<_>>(),
489            type_map,
490            s,
491        )?,
492        StructFields::Named(named) => {
493            let fields = skip_fields_named(named.fields()).collect::<Vec<_>>();
494
495            if fields.is_empty() {
496                return Ok(match named.tag().as_ref() {
497                    Some(tag) => write!(s, r#"{{ "{tag}": "{key}" }}"#)?,
498                    None => write!(s, "Record<{STRING}, {NEVER}>")?,
499                });
500            }
501
502            let (flattened, non_flattened): (Vec<_>, Vec<_>) =
503                fields.iter().partition(|(_, (f, _))| f.flatten());
504
505            let mut field_sections = flattened
506                .into_iter()
507                .map(|(key, (field, ty))| {
508                    let mut s = String::new();
509                    datatype_inner(
510                        ctx.with(PathItem::Field(key.clone())),
511                        &FunctionResultVariant::Value(ty.clone()),
512                        type_map,
513                        &mut s,
514                    )
515                    .map(|_| {
516                        inner_comments(
517                            ctx.clone(),
518                            field.deprecated(),
519                            field.docs(),
520                            format!("({s})"),
521                            true,
522                        )
523                    })
524                })
525                .collect::<Result<Vec<_>>>()?;
526
527            let mut unflattened_fields = non_flattened
528                .into_iter()
529                .map(|(key, field_ref)| {
530                    let (field, _) = field_ref;
531
532                    let mut other = String::new();
533                    object_field_to_ts(
534                        ctx.with(PathItem::Field(key.clone())),
535                        key.clone(),
536                        field_ref,
537                        type_map,
538                        &mut other,
539                    )?;
540
541                    Ok(inner_comments(
542                        ctx.clone(),
543                        field.deprecated(),
544                        field.docs(),
545                        other,
546                        true,
547                    ))
548                })
549                .collect::<Result<Vec<_>>>()?;
550
551            if let Some(tag) = &named.tag() {
552                unflattened_fields.push(format!("{tag}: \"{key}\""));
553            }
554
555            if !unflattened_fields.is_empty() {
556                field_sections.push(format!("{{ {} }}", unflattened_fields.join("; ")));
557            }
558
559            s.push_str(&field_sections.join(" & "));
560        }
561    })
562}
563
564fn enum_variant_datatype(
565    ctx: ExportContext,
566    type_map: &TypeCollection,
567    name: Cow<'static, str>,
568    variant: &EnumVariant,
569) -> Result<Option<String>> {
570    match &variant.inner() {
571        // TODO: Remove unreachable in type system
572        EnumVariants::Unit => unreachable!("Unit enum variants have no type!"),
573        EnumVariants::Named(obj) => {
574            let mut fields = if let Some(tag) = &obj.tag() {
575                let sanitised_name = sanitise_key(name, true);
576                vec![format!("{tag}: {sanitised_name}")]
577            } else {
578                vec![]
579            };
580
581            fields.extend(
582                skip_fields_named(obj.fields())
583                    .map(|(name, field_ref)| {
584                        let (field, _) = field_ref;
585
586                        let mut other = String::new();
587                        object_field_to_ts(
588                            ctx.with(PathItem::Field(name.clone())),
589                            name.clone(),
590                            field_ref,
591                            type_map,
592                            &mut other,
593                        )?;
594
595                        Ok(inner_comments(
596                            ctx.clone(),
597                            field.deprecated(),
598                            field.docs(),
599                            other,
600                            true,
601                        ))
602                    })
603                    .collect::<Result<Vec<_>>>()?,
604            );
605
606            Ok(Some(match &fields[..] {
607                [] => format!("Record<{STRING}, {NEVER}>").to_string(),
608                fields => format!("{{ {} }}", fields.join("; ")),
609            }))
610        }
611        EnumVariants::Unnamed(obj) => {
612            let fields = skip_fields(obj.fields())
613                .map(|(_, ty)| {
614                    let mut s = String::new();
615                    datatype_inner(
616                        ctx.clone(),
617                        &FunctionResultVariant::Value(ty.clone()),
618                        type_map,
619                        &mut s,
620                    )
621                    .map(|_| s)
622                })
623                .collect::<Result<Vec<_>>>()?;
624
625            Ok(match &fields[..] {
626                [] => {
627                    // If the actual length is 0, we know `#[serde(skip)]` was not used.
628                    if obj.fields().is_empty() {
629                        Some("[]".to_string())
630                    } else {
631                        // We wanna render `{tag}` not `{tag}: {type}` (where `{type}` is what this function returns)
632                        None
633                    }
634                }
635                // If the actual length is 1, we know `#[serde(skip)]` was not used.
636                [field] if obj.fields().len() == 1 => Some(field.to_string()),
637                fields => Some(format!("[{}]", fields.join(", "))),
638            })
639        }
640    }
641}
642
643fn enum_datatype(
644    ctx: ExportContext,
645    e: &EnumType,
646    type_map: &TypeCollection,
647    s: &mut String,
648) -> Result<()> {
649    if e.variants().is_empty() {
650        return Ok(write!(s, "{NEVER}")?);
651    }
652
653    Ok(match &e.repr() {
654        EnumRepr::Untagged => {
655            let mut variants = e
656                .variants()
657                .iter()
658                .filter(|(_, variant)| !variant.skip())
659                .map(|(name, variant)| {
660                    Ok(match variant.inner() {
661                        EnumVariants::Unit => NULL.to_string(),
662                        _ => inner_comments(
663                            ctx.clone(),
664                            variant.deprecated(),
665                            variant.docs(),
666                            enum_variant_datatype(
667                                ctx.with(PathItem::Variant(name.clone())),
668                                type_map,
669                                name.clone(),
670                                variant,
671                            )?
672                            .expect("Invalid Serde type"),
673                            true,
674                        ),
675                    })
676                })
677                .collect::<Result<Vec<_>>>()?;
678            variants.dedup();
679            s.push_str(&variants.join(" | "));
680        }
681        repr => {
682            let mut variants = e
683                .variants()
684                .iter()
685                .filter(|(_, variant)| !variant.skip())
686                .map(|(variant_name, variant)| {
687                    let sanitised_name = sanitise_key(variant_name.clone(), true);
688
689                    Ok(inner_comments(
690                        ctx.clone(),
691                        variant.deprecated(),
692                        variant.docs(),
693                        match (repr, &variant.inner()) {
694                            (EnumRepr::Untagged, _) => unreachable!(),
695                            (EnumRepr::Internal { tag }, EnumVariants::Unit) => {
696                                format!("{{ {tag}: {sanitised_name} }}")
697                            }
698                            (EnumRepr::Internal { tag }, EnumVariants::Unnamed(tuple)) => {
699                                let fields = skip_fields(tuple.fields()).collect::<Vec<_>>();
700
701                                // This field is only required for `{ty}` not `[...]` so we only need to check when there one field
702                                let dont_join_ty = if tuple.fields().len() == 1 {
703                                    let (_, ty) = fields.first().expect("checked length above");
704                                    validate_type_for_tagged_intersection(
705                                        ctx.clone(),
706                                        (**ty).clone(),
707                                        type_map,
708                                    )?
709                                } else {
710                                    false
711                                };
712
713                                let mut typ = String::new();
714
715                                unnamed_fields_datatype(ctx.clone(), &fields, type_map, &mut typ)?;
716
717                                if dont_join_ty {
718                                    format!("({{ {tag}: {sanitised_name} }})")
719                                } else {
720                                    // We wanna be sure `... & ... | ...` becomes `... & (... | ...)`
721                                    if typ.contains('|') {
722                                        typ = format!("({typ})");
723                                    }
724                                    format!("({{ {tag}: {sanitised_name} }} & {typ})")
725                                }
726                            }
727                            (EnumRepr::Internal { tag }, EnumVariants::Named(obj)) => {
728                                let mut fields = vec![format!("{tag}: {sanitised_name}")];
729
730                                for (name, field) in skip_fields_named(obj.fields()) {
731                                    let mut other = String::new();
732                                    object_field_to_ts(
733                                        ctx.with(PathItem::Field(name.clone())),
734                                        name.clone(),
735                                        field,
736                                        type_map,
737                                        &mut other,
738                                    )?;
739                                    fields.push(other);
740                                }
741
742                                format!("{{ {} }}", fields.join("; "))
743                            }
744                            (EnumRepr::External, EnumVariants::Unit) => sanitised_name.to_string(),
745                            (EnumRepr::External, _) => {
746                                let ts_values = enum_variant_datatype(
747                                    ctx.with(PathItem::Variant(variant_name.clone())),
748                                    type_map,
749                                    variant_name.clone(),
750                                    variant,
751                                )?;
752                                let sanitised_name = sanitise_key(variant_name.clone(), false);
753
754                                match ts_values {
755                                    Some(ts_values) => {
756                                        format!("{{ {sanitised_name}: {ts_values} }}")
757                                    }
758                                    None => format!(r#""{sanitised_name}""#),
759                                }
760                            }
761                            (EnumRepr::Adjacent { tag, .. }, EnumVariants::Unit) => {
762                                format!("{{ {tag}: {sanitised_name} }}")
763                            }
764                            (EnumRepr::Adjacent { tag, content }, _) => {
765                                let ts_value = enum_variant_datatype(
766                                    ctx.with(PathItem::Variant(variant_name.clone())),
767                                    type_map,
768                                    variant_name.clone(),
769                                    variant,
770                                )?;
771
772                                let mut s = String::new();
773
774                                s.push_str("{ ");
775
776                                write!(s, "{tag}: {sanitised_name}")?;
777                                if let Some(ts_value) = ts_value {
778                                    write!(s, "; {content}: {ts_value}")?;
779                                }
780
781                                s.push_str(" }");
782
783                                s
784                            }
785                        },
786                        true,
787                    ))
788                })
789                .collect::<Result<Vec<_>>>()?;
790            variants.dedup();
791            s.push_str(&variants.join(" | "));
792        }
793    })
794}
795
796// impl std::fmt::Display for LiteralType {
797//     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
798//         match self {
799//             Self::i8(v) => write!(f, "{v}"),
800//             Self::i16(v) => write!(f, "{v}"),
801//             Self::i32(v) => write!(f, "{v}"),
802//             Self::u8(v) => write!(f, "{v}"),
803//             Self::u16(v) => write!(f, "{v}"),
804//             Self::u32(v) => write!(f, "{v}"),
805//             Self::f32(v) => write!(f, "{v}"),
806//             Self::f64(v) => write!(f, "{v}"),
807//             Self::bool(v) => write!(f, "{v}"),
808//             Self::String(v) => write!(f, r#""{v}""#),
809//             Self::char(v) => write!(f, r#""{v}""#),
810//             Self::None => f.write_str(NULL),
811//         }
812//     }
813// }
814
815/// convert an object field into a Typescript string
816fn object_field_to_ts(
817    ctx: ExportContext,
818    key: Cow<'static, str>,
819    (field, ty): NonSkipField,
820    type_map: &TypeCollection,
821    s: &mut String,
822) -> Result<()> {
823    let field_name_safe = sanitise_key(key, false);
824
825    // https://github.com/oscartbeaumont/rspc/issues/100#issuecomment-1373092211
826    let (key, ty) = match field.optional() {
827        true => (format!("{field_name_safe}?").into(), ty),
828        false => (field_name_safe, ty),
829    };
830
831    let mut value = String::new();
832    datatype_inner(
833        ctx,
834        &FunctionResultVariant::Value(ty.clone()),
835        type_map,
836        &mut value,
837    )?;
838
839    Ok(write!(s, "{key}: {value}",)?)
840}
841
842/// sanitise a string to be a valid Typescript key
843fn sanitise_key<'a>(field_name: Cow<'static, str>, force_string: bool) -> Cow<'a, str> {
844    let valid = field_name
845        .chars()
846        .all(|c| c.is_alphanumeric() || c == '_' || c == '$')
847        && field_name
848            .chars()
849            .next()
850            .map(|first| !first.is_numeric())
851            .unwrap_or(true);
852
853    if force_string || !valid {
854        format!(r#""{field_name}""#).into()
855    } else {
856        field_name
857    }
858}
859
860pub(crate) fn sanitise_type_name(ctx: ExportContext, loc: NamedLocation, ident: &str) -> Output {
861    if let Some(name) = RESERVED_TYPE_NAMES.iter().find(|v| **v == ident) {
862        return Err(ExportError::ForbiddenName(loc, ctx.export_path(), name));
863    }
864
865    if let Some(first_char) = ident.chars().next() {
866        if !first_char.is_alphabetic() && first_char != '_' {
867            return Err(ExportError::InvalidName(
868                loc,
869                ctx.export_path(),
870                ident.to_string(),
871            ));
872        }
873    }
874
875    if ident
876        .find(|c: char| !c.is_alphanumeric() && c != '_')
877        .is_some()
878    {
879        return Err(ExportError::InvalidName(
880            loc,
881            ctx.export_path(),
882            ident.to_string(),
883        ));
884    }
885
886    Ok(ident.to_string())
887}
888
889fn validate_type_for_tagged_intersection(
890    ctx: ExportContext,
891    ty: DataType,
892    type_map: &TypeCollection,
893) -> Result<bool> {
894    match ty {
895        DataType::Any
896        | DataType::Unknown
897        | DataType::Primitive(_)
898        // `T & null` is `never` but `T & (U | null)` (this variant) is `T & U` so it's fine.
899        | DataType::Nullable(_)
900        | DataType::List(_)
901        | DataType::Map(_)
902        | DataType::Generic(_) => Ok(false),
903        DataType::Literal(v) => match v {
904            LiteralType::None => Ok(true),
905            _ => Ok(false),
906        },
907        DataType::Struct(v) => match v.fields() {
908            StructFields::Unit => Ok(true),
909            StructFields::Unnamed(_) => {
910                Err(ExportError::InvalidTaggedVariantContainingTupleStruct(
911                   ctx.export_path()
912                ))
913            }
914            StructFields::Named(fields) => {
915                // Prevent `{ tag: "{tag}" } & Record<string | never>`
916                if fields.tag().is_none() && fields.fields().is_empty() {
917                    return Ok(true);
918                }
919
920                Ok(false)
921            }
922        },
923        DataType::Enum(v) => {
924            match v.repr() {
925                EnumRepr::Untagged => {
926                    Ok(v.variants().iter().any(|(_, v)| match &v.inner() {
927                        // `{ .. } & null` is `never`
928                        EnumVariants::Unit => true,
929                         // `{ ... } & Record<string, never>` is not useful
930                        EnumVariants::Named(v) => v.tag().is_none() && v.fields().is_empty(),
931                        EnumVariants::Unnamed(_) => false,
932                    }))
933                },
934                // All of these repr's are always objects.
935                EnumRepr::Internal { .. } | EnumRepr::Adjacent { .. } | EnumRepr::External => Ok(false),
936            }
937        }
938        DataType::Tuple(v) => {
939            // Empty tuple is `null`
940            if v.elements().is_empty() {
941                return Ok(true);
942            }
943
944            Ok(false)
945        }
946        DataType::Reference(r) => validate_type_for_tagged_intersection(
947            ctx,
948            type_map
949                .get(r.sid())
950                .expect("TypeCollection should have been populated by now")
951                .inner
952                .clone(),
953            type_map,
954        ),
955    }
956}
957
958const ANY: &str = "any";
959const UNKNOWN: &str = "unknown";
960const NUMBER: &str = "number";
961const STRING: &str = "string";
962const BOOLEAN: &str = "boolean";
963const NULL: &str = "null";
964const NEVER: &str = "never";
965const BIGINT: &str = "bigint";