ADR-0007: @dataclass Transformation Strategy
ADR-0007: @dataclass Transformation Strategy
Status
Accepted
Context
Python’s @dataclass decorator (PEP 557) automatically generates boilerplate methods for classes
that are primarily data containers. It’s one of Python’s most popular features for reducing
boilerplate.
from dataclasses import dataclass
@dataclassclass Point: x: int y: intThis automatically generates __init__, __repr__, __eq__, and other methods.
We need to decide how to handle @dataclass in our transpiler:
-
Generic decorator wrapping: Treat it like any other decorator
const Point = dataclass(class Point { ... })- Requires a runtime
dataclassfunction that does reflection magic - Complex, fragile, doesn’t leverage TypeScript’s type system
-
Special-case transformation: Recognize
@dataclassand generate equivalent TypeScript- Generate typed fields and constructor directly
- No runtime dependency for basic functionality
- Cleaner, more idiomatic TypeScript output
-
Ignore the decorator: Strip
@dataclassand just emit the class body- Loses the auto-generated constructor
- Would require manual constructor definition in Python source
Decision
We will implement special-case transformation for @dataclass. The decorator is recognized
during transformation and the class is rewritten to include:
- Typed field declarations
- Auto-generated constructor with parameters matching fields
- Support for
frozen=TrueviareadonlyandObject.freeze() - Support for
field(default_factory=...)for mutable defaults
Basic Transformation
@dataclassclass Person: name: str age: int email: str = ""class Person { name: string age: number email: string
constructor(name: string, age: number, email: string = "") { this.name = name this.age = age this.email = email }}Frozen Dataclasses
@dataclass(frozen=True)class Point: x: int y: intclass Point { readonly x: number readonly y: number
constructor(x: number, y: number) { this.x = x this.y = y Object.freeze(this) }}Field with default_factory
Python disallows mutable default values. The field(default_factory=...) pattern solves this:
@dataclassclass Container: items: list = field(default_factory=list) metadata: dict = field(default_factory=dict) tags: set = field(default_factory=set)class Container { items: unknown[] metadata: Record<string, unknown> tags: Set<unknown>
constructor( items: unknown[] = [], metadata: Record<string, unknown> = {}, tags: Set<unknown> = new Set() ) { this.items = items this.metadata = metadata this.tags = tags }}Inheritance
Dataclasses that inherit from other dataclasses call super():
@dataclassclass Employee(Person): employee_id: strclass Employee extends Person { employee_id: string
constructor(name: string, age: number, email: string = "", employee_id: string) { super(name, age, email) this.employee_id = employee_id }}What We Don’t Transform
These @dataclass features are not specially handled (they’re either ignored or would need runtime
support):
__post_init__method (transformed as regular method, but not auto-called)__eq__,__repr__,__hash__generation (not needed for most TypeScript use cases)order=Truefor comparison operatorsslots=True(JavaScript doesn’t have slots)ClassVarfields (recognized and excluded from constructor)InitVarfields (included in constructor but not as class field)
Generic Decorators vs @dataclass
Other class decorators use the generic wrapping pattern:
@register@validateclass MyClass: passconst MyClass = register(validate(class MyClass {}))The key distinction is that @dataclass fundamentally changes the class structure (adding
constructor logic), while generic decorators wrap the class without modifying its internals.
Consequences
Positive
- Idiomatic TypeScript: Generated code looks like hand-written TypeScript
- Type safety: Constructor parameters are fully typed
- No runtime dependency: Basic dataclass functionality needs no
py.*helpers - IDE support: TypeScript understands the class structure completely
- Immutability support:
frozen=Truemaps naturally toreadonly
Negative
- Incomplete feature coverage: Advanced dataclass features aren’t supported
- Behavioral differences: Generated
__eq__would behave differently than Python’s - Special-case complexity: Parser must recognize
@dataclassspecifically - Decorator order matters:
@dataclassmust be handled before other decorators
Design Rationale
The decision to special-case @dataclass rather than implement a runtime equivalent comes down to
TypeScript’s strengths:
-
Static typing: TypeScript’s value is in compile-time type checking. A runtime
dataclassfunction couldn’t provide the same level of type inference. -
Tooling integration: IDEs understand TypeScript classes natively. A decorator-wrapped class would lose autocomplete for fields.
-
Simplicity: Generating a constructor is straightforward. Implementing full dataclass semantics at runtime would be complex and error-prone.
-
Common use case: Most dataclass usage is for simple data containers. The generated TypeScript handles this case well.