Summary
TypeMapping(...) is silently ignored for any type that implements a custom JSON marshaler. The source provider rewrites such types to unknown during the type walk, before the emit-layer mappings are applied — and the provider never receives the user's TypeMappings at all. This makes it impossible to generate TypeScript for Go APIs built on k8s.io/apimachinery (and similar libraries), where time types like metav1.MicroTime/metav1.Time marshal to RFC3339 strings.
This is the exact scenario tygo's type_mappings: handles by pruning the walk (e.g. metav1.MicroTime: "string"). tygorgen's TypeMapping does not prune.
Repro
package main
import (
v1alpha1 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
"tygor.dev/tygorgen"
)
func main() {
// v1alpha1.UIResourceStatus.lastDeployTime is metav1.MicroTime (custom marshaler → string)
_, _ = tygorgen.FromTypes(v1alpha1.UIResource{}).
Provider("source").
SingleFile().
TypeMapping("metav1.MicroTime", "string"). // <-- ignored
TypeMapping("metav1.Time", "string"). // <-- ignored
ToDir("./out")
}
Actual
warning: CUSTOM_MARSHALER: type MicroTime implements custom marshaler, mapped to 'unknown'
export interface UIResourceStatus {
lastDeployTime?: unknown; // expected: string
pendingBuildSince?: unknown;
// ...
}
Expected
The explicit TypeMapping("metav1.MicroTime", "string") should take precedence, yielding lastDeployTime?: string.
Root cause
tygorgen/provider/source_provider.go (convert named type):
// Check for custom marshalers on the named type itself
if b.hasCustomMarshaler(named) {
// ...
// Create an alias to PrimitiveAny
aliasDesc := &ir.AliasDescriptor{
Name: ir.GoIdentifier{Name: tn.Name(), Package: pkgPath},
Underlying: ir.Any(),
// ...
}
b.schema.AddType(aliasDesc)
return nil
}
SourceInputOptions only carries Packages and RootTypes — TypeMappings are passed to the emitter, not the provider — so the walk cannot consult them. Two related symptoms fall out of the same gap:
- Custom-marshaler types →
unknown even with an explicit mapping (above).
- Mapped external structs are still fully walked and emitted. Mapping
metav1.ObjectMeta → ObjectMeta (defined in frontmatter) still emits a second ObjectMeta from k8s source plus its transitive junk (ManagedFieldsEntry, OwnerReference, FieldsV1, …), producing duplicate-identifier output. tygo avoids this because type_mappings prune the walk.
Why tygo doesn't hit this
tygo is syntactic and per-package. It walks the AST of the configured package dirs and emits only types textually declared there. A field of type metav1.MicroTime is an *ast.SelectorExpr, and tygo simply substitutes the name (tygo/write.go):
case *ast.SelectorExpr: // e.g. `time.Time`
longType := fmt.Sprintf("%s.%s", t.X, t.Sel) // "metav1.MicroTime"
mappedTsType, ok := g.conf.TypeMappings[longType]
if ok { s.WriteString(mappedTsType) } // -> "string"
else { /* fallback `any /* metav1.MicroTime */` */ }
It never resolves metav1.MicroTime to its definition, never recurses into apimachinery, and never sees the custom marshaler. The mapping is a pruning boundary by construction.
tygorgen's source provider is semantic (go/types): it resolves and walks the full reachable type graph, introspects marshalers, and emits reachable types. Because mappings are emit-only, the walk reaches external types it should have treated as opaque.
Suggested fix
Thread TypeMappings into SourceInputOptions and consult them in convertNamedType (and the struct-field walk): if a named type matches a user mapping, emit a mapped reference and do not descend into or emit the type. This makes TypeMapping a true pruning boundary, matching tygo semantics.
Workaround
Vendor the upstream tygo-generated .d.ts (which already maps these correctly) until mappings prune the source walk.
Environment
- tygor
v0.8.6
metav1 types from k8s.io/apimachinery (via github.com/tilt-dev/tilt pkg/apis/core/v1alpha1)
Summary
TypeMapping(...)is silently ignored for any type that implements a custom JSON marshaler. Thesourceprovider rewrites such types tounknownduring the type walk, before the emit-layer mappings are applied — and the provider never receives the user'sTypeMappingsat all. This makes it impossible to generate TypeScript for Go APIs built onk8s.io/apimachinery(and similar libraries), where time types likemetav1.MicroTime/metav1.Timemarshal to RFC3339 strings.This is the exact scenario tygo's
type_mappings:handles by pruning the walk (e.g.metav1.MicroTime: "string"). tygorgen'sTypeMappingdoes not prune.Repro
Actual
Expected
The explicit
TypeMapping("metav1.MicroTime", "string")should take precedence, yieldinglastDeployTime?: string.Root cause
tygorgen/provider/source_provider.go(convert named type):SourceInputOptionsonly carriesPackagesandRootTypes—TypeMappingsare passed to the emitter, not the provider — so the walk cannot consult them. Two related symptoms fall out of the same gap:unknowneven with an explicit mapping (above).metav1.ObjectMeta → ObjectMeta(defined in frontmatter) still emits a secondObjectMetafrom k8s source plus its transitive junk (ManagedFieldsEntry,OwnerReference,FieldsV1, …), producing duplicate-identifier output. tygo avoids this becausetype_mappingsprune the walk.Why tygo doesn't hit this
tygo is syntactic and per-package. It walks the AST of the configured package dirs and emits only types textually declared there. A field of type
metav1.MicroTimeis an*ast.SelectorExpr, and tygo simply substitutes the name (tygo/write.go):It never resolves
metav1.MicroTimeto its definition, never recurses into apimachinery, and never sees the custom marshaler. The mapping is a pruning boundary by construction.tygorgen's
sourceprovider is semantic (go/types): it resolves and walks the full reachable type graph, introspects marshalers, and emits reachable types. Because mappings are emit-only, the walk reaches external types it should have treated as opaque.Suggested fix
Thread
TypeMappingsintoSourceInputOptionsand consult them inconvertNamedType(and the struct-field walk): if a named type matches a user mapping, emit a mapped reference and do not descend into or emit the type. This makesTypeMappinga true pruning boundary, matching tygo semantics.Workaround
Vendor the upstream tygo-generated
.d.ts(which already maps these correctly) until mappings prune the source walk.Environment
v0.8.6metav1types fromk8s.io/apimachinery(viagithub.com/tilt-dev/tiltpkg/apis/core/v1alpha1)