typewriter
Cross-Language Type Synchronization SDK for Rust
Define your types once in Rust. Get perfectly matching types in TypeScript, Python, Go, Swift, Kotlin, GraphQL, and JSON Schema — automatically, forever.
Features
- Annotate once → generate everywhere: Add
#[derive(TypeWriter)]to your Rust structs/enums - Zero drift: Types stay in sync automatically on every build
- Multiple languages: TypeScript, Python, Go, Swift, Kotlin, GraphQL, JSON Schema
- Zod schemas: Automatic validation schemas for TypeScript
- CLI tools: Project-wide generation, drift checking, watch mode
Quick Example
#![allow(unused)]
fn main() {
use typebridge::TypeWriter;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, TypeWriter)]
#[sync_to(typescript, python, go)]
pub struct User {
pub id: String,
pub email: String,
pub name: String,
pub age: Option<u32>,
}
}
This generates:
TypeScript:
export interface User {
id: string;
email: string;
name: string;
age?: number | undefined;
}
Python:
class User(BaseModel):
id: str
email: str
name: str
age: Optional[int] = None
Go:
type User struct {
Id string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Age *uint32 `json:"age,omitempty"`
}
Next Steps
- Installation — Add typewriter to your project
- Your First Type — Generate your first types
- Language Guides — Language-specific details
Resources
Installation
Prerequisites
- Rust stable toolchain (1.70+)
- A Cargo project with
serdefor serialization
Add Dependencies
Add typebridge and serde to your Cargo.toml:
[dependencies]
typebridge = "0.5.0"
serde = { version = "1", features = ["derive"] }
By default, all language emitters are enabled:
- TypeScript
- Python
- Go
- Swift
- Kotlin
- GraphQL
- JSON Schema
Feature Flags
To reduce compile times, enable only the languages you need:
# TypeScript only
typebridge = { version = "0.5.0", default-features = false, features = ["typescript"] }
# TypeScript + Python
typebridge = { version = "0.5.0", default-features = false, features = ["typescript", "python"] }
# All languages (default)
typebridge = "0.5.0"
CLI Installation
For project-wide generation, drift checking, and watch mode:
cargo install typebridge-cli
Next Steps
- Your First Type — Generate your first types
- Configuration — Customize output directories and options
Your First Type
Basic Struct
Add your first #[derive(TypeWriter)] struct:
#![allow(unused)]
fn main() {
use typebridge::TypeWriter;
use serde::{Serialize, Deserialize};
/// A user in the system.
#[derive(Serialize, Deserialize, TypeWriter)]
#[sync_to(typescript, python, go)]
pub struct User {
pub id: String,
pub email: String,
pub name: String,
pub age: Option<u32>,
pub is_active: bool,
pub tags: Vec<String>,
}
}
Building
Run cargo build to generate the types:
cargo build
You’ll see output like:
typewriter: User → ./generated/typescript/user.ts
typewriter: User → ./generated/typescript/user.schema.ts
typewriter: User → ./generated/python/user.py
typewriter: User → ./generated/go/user.go
Generated Output
TypeScript
export interface User {
id: string;
email: string;
name: string;
age?: number | undefined;
is_active: boolean;
tags: string[];
}
Python
from pydantic import BaseModel
from typing import Optional
class User(BaseModel):
id: str
email: str
name: str
age: Optional[int] = None
is_active: bool
tags: list[str]
Go
package types
type User struct {
Id string `json:"id"`
Email string `json:"email"`
Name string `json:"name"`
Age *uint32 `json:"age,omitempty"`
Is_active bool `json:"is_active"`
Tags []string `json:"tags"`
}
What Each Part Does
| Part | Purpose |
|---|---|
use typebridge::TypeWriter; | Import the derive macro |
#[derive(... TypeWriter)] | Enable type generation |
#[sync_to(typescript, python)] | Target languages |
/// A user... | Doc comment becomes JSDoc/docstring |
Next Steps
- Configuration — Customize output directories
- Type Mappings — See all supported types
- Attributes Reference — Fine-tune output
Configuration
Create a typewriter.toml at your project root to customize output directories, file naming styles, and other options.
Basic Configuration
[typescript]
output_dir = "../frontend/src/types"
file_style = "kebab-case"
readonly = false
zod = true
[python]
output_dir = "../api/schemas"
file_style = "snake_case"
pydantic_v2 = true
[go]
output_dir = "../backend/types"
package_name = "api_types"
[graphql]
output_dir = "../schema/types"
[kotlin]
package_name = "com.example.types"
All Configuration Options
TypeScript
[typescript]
output_dir = "./generated/typescript" # Output directory
file_style = "kebab-case" # kebab-case | snake_case | PascalCase
readonly = false # Make all fields readonly
zod = true # Generate Zod schema files
Python
[python]
output_dir = "./generated/python"
file_style = "snake_case" # snake_case | kebab-case | PascalCase
pydantic_v2 = true # Use Pydantic v2
use_dataclass = false # Use @dataclass instead of BaseModel
Go
[go]
output_dir = "./generated/go"
file_style = "snake_case"
package_name = "types" # Go package name
Swift
[swift]
output_dir = "./generated/swift"
file_style = "PascalCase" # PascalCase | snake_case | kebab-case
Kotlin
[kotlin]
output_dir = "./generated/kotlin"
file_style = "PascalCase"
package_name = "types" # Kotlin package name
GraphQL
[graphql]
output_dir = "./generated/graphql"
file_style = "snake_case"
JSON Schema
[json_schema]
output_dir = "./generated/json-schema"
file_style = "snake_case"
File Naming Styles
| Style | Example |
|---|---|
kebab-case | user-profile.ts |
snake_case | user_profile.py |
PascalCase | UserProfile.swift |
Default Values
If not specified:
| Setting | Default |
|---|---|
| TypeScript output_dir | ./generated/typescript |
| Python output_dir | ./generated/python |
| Go output_dir | ./generated/go |
| Swift output_dir | ./generated/swift |
| Kotlin output_dir | ./generated/kotlin |
| GraphQL output_dir | ./generated/graphql |
| JSON Schema output_dir | ./generated/json-schema |
| TypeScript zod | true |
| Python pydantic_v2 | true |
| Go package_name | types |
| Kotlin package_name | types |
TypeScript
Generates TypeScript interfaces and optionally Zod validation schemas.
Type Mappings
| Rust Type | TypeScript Type |
|---|---|
String | string |
bool | boolean |
u8, u16, i8, i16 | number |
u32, i32, f32, f64 | number |
u64, i64, u128, i128 | bigint |
Uuid | string |
DateTime<Utc> | string |
Option<T> | T | undefined |
Vec<T> | T[] |
HashMap<K, V> | Record<K, V> |
Example
Rust:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(typescript)]
pub struct User {
pub id: String,
pub email: String,
pub age: Option<u32>,
}
}
Generated:
export interface User {
id: string;
email: string;
age?: number | undefined;
}
Zod Schemas
Zod schemas are generated by default alongside interfaces. They provide runtime validation:
import { z } from 'zod';
export const UserSchema = z.object({
"id": z.string(),
"email": z.string(),
"age": z.number().optional(),
});
Disable Zod schema generation:
[typescript]
zod = false
Or per-type:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(typescript)]
#[tw(zod = false)]
pub struct InternalOnly {
// ...
}
}
Readonly Fields
Make all fields readonly:
[typescript]
readonly = true
export interface User {
readonly id: string;
readonly email: string;
}
File Naming
Files use kebab-case by default: UserProfile → user-profile.ts
Change style in typewriter.toml:
[typescript]
file_style = "snake_case" # user_profile.ts
# or
file_style = "PascalCase" # UserProfile.ts
Python
Generates Python Pydantic v2 models.
Type Mappings
| Rust Type | Python Type |
|---|---|
String | str |
bool | bool |
u8, u16, u32, u64, i8, i16, i32, i64 | int |
f32, f64 | float |
Uuid | UUID |
DateTime<Utc> | datetime |
Option<T> | Optional[T] = None |
Vec<T> | list[T] |
HashMap<K, V> | dict[K, V] |
Example
Rust:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(python)]
pub struct User {
pub id: String,
pub email: String,
pub age: Option<u32>,
}
}
Generated:
from pydantic import BaseModel
from typing import Optional
class User(BaseModel):
id: str
email: str
age: Optional[int] = None
Dataclass Mode
Use Python dataclasses instead of Pydantic:
[python]
use_dataclass = true
pydantic_v2 = false
from dataclasses import dataclass
from typing import Optional
@dataclass
class User:
id: str
email: str
age: Optional[int] = None
File Naming
Files use snake_case by default: UserProfile → user_profile.py
[python]
file_style = "kebab-case" # user-profile.py
# or
file_style = "PascalCase" # UserProfile.py
Go
Generates Go structs with JSON tags.
Type Mappings
| Rust Type | Go Type |
|---|---|
String | string |
bool | bool |
u8, u16, u32 | uint8, uint16, uint32 |
u64 | uint64 |
i8, i16, i32, i64 | int8, int16, int32, int64 |
f32, f64 | float32, float64 |
Uuid | string |
DateTime<Utc> | time.Time |
Option<T> | *T with omitempty |
Vec<T> | []T |
HashMap<K, V> | map[K]V |
Example
Rust:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(go)]
pub struct User {
pub id: String,
pub email: String,
pub age: Option<u32>,
}
}
Generated:
package types
type User struct {
Id string `json:"id"`
Email string `json:"email"`
Age *uint32 `json:"age,omitempty"`
}
Package Name
Configure the Go package name:
[go]
package_name = "models"
File Naming
Files use snake_case by default: UserProfile → user_profile.go
[go]
file_style = "PascalCase" # UserProfile.go
Swift
Generates Swift structs with Codable protocol.
Type Mappings
| Rust Type | Swift Type |
|---|---|
String | String |
bool | Bool |
u8, u16, u32, u64 | UInt8, UInt16, UInt32, UInt64 |
i8, i16, i32, i64 | Int8, Int16, Int32, Int64 |
f32, f64 | Float, Double |
Uuid | UUID |
DateTime<Utc> | Date |
Option<T> | T? |
Vec<T> | [T] |
HashMap<K, V> | [K: V] |
Example
Rust:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(swift)]
pub struct User {
pub id: String,
pub email: String,
pub age: Option<u32>,
}
}
Generated:
struct User: Codable {
let id: String
let email: String
let age: UInt32?
}
File Naming
Files use PascalCase by default: UserProfile → UserProfile.swift
[swift]
file_style = "snake_case" # user_profile.swift
Kotlin
Generates Kotlin data classes with kotlinx.serialization.
Type Mappings
| Rust Type | Kotlin Type |
|---|---|
String | String |
bool | Boolean |
u8, u16, u32, u64 | UByte, UShort, UInt, ULong |
i8, i16, i32, i64 | Byte, Short, Int, Long |
f32, f64 | Float, Double |
Uuid | String |
DateTime<Utc> | kotlinx.datetime.Instant |
Option<T> | T? = null |
Vec<T> | List<T> |
HashMap<K, V> | Map<K, V> |
Example
Rust:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(kotlin)]
pub struct User {
pub id: String,
pub email: String,
pub age: Option<u32>,
}
}
Generated:
package types
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class User(
@SerialName("id") val id: String,
@SerialName("email") val email: String,
@SerialName("age") val age: Int? = null
)
Package Name
Configure the Kotlin package name:
[kotlin]
package_name = "com.example.models"
File Naming
Files use PascalCase by default: UserProfile → UserProfile.kt
[kotlin]
file_style = "snake_case" # user_profile.kt
GraphQL
Generates GraphQL Schema Definition Language (SDL) types.
Type Mappings
| Rust Type | GraphQL Type |
|---|---|
String | String |
bool | Boolean |
u8, u16, u32, i8, i16, i32 | Int |
f32, f64 | Float |
Uuid | ID |
DateTime<Utc> | DateTime (custom scalar) |
Option<T> | Nullable (no !) |
Vec<T> | [T!] |
HashMap<K, V> | JSON (custom scalar) |
Example
Rust:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(graphql)]
pub struct User {
pub id: String,
pub email: String,
pub age: Option<u32>,
}
}
Generated:
scalar DateTime
scalar JSON
type User {
id: ID!
email: String!
age: Int
}
Enums
Simple enums become GraphQL enums:
Rust:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(graphql)]
pub enum Role {
Admin,
User,
Guest,
}
}
GraphQL:
enum Role {
ADMIN
USER
GUEST
}
File Naming
Files use snake_case by default: UserProfile → user_profile.graphql
[graphql]
file_style = "kebab-case" # user-profile.graphql
JSON Schema
Generates JSON Schema Draft 2020-12 definitions.
Type Mappings
| Rust Type | JSON Schema Type |
|---|---|
String | { "type": "string" } |
bool | { "type": "boolean" } |
u8-u32, i8-i32 | { "type": "integer" } |
u64, i64, u128, i128 | { "type": "integer" } (64-bit) |
f32, f64 | { "type": "number" } |
Uuid | { "type": "string", "format": "uuid" } |
DateTime<Utc> | { "type": "string", "format": "date-time" } |
NaiveDate | { "type": "string", "format": "date" } |
Option<T> | Not in required array |
Vec<T> | { "type": "array", "items": {...} } |
HashMap<K, V> | { "type": "object", "additionalProperties": {...} } |
Example
Rust:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(json_schema)]
pub struct User {
pub id: String,
pub email: String,
pub age: Option<u32>,
}
}
Generated:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": { "type": "string", "format": "uuid" },
"email": { "type": "string" },
"age": { "type": "integer" }
},
"required": ["id", "email"],
"additionalProperties": false
}
File Naming
Files use snake_case by default: UserProfile → user_profile.schema.json
[json_schema]
file_style = "kebab-case" # user-profile.schema.json
Attributes Reference
#[sync_to(...)]
Specifies which languages to generate types for:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(typescript, python, go, swift, kotlin, graphql, json_schema)]
pub struct MyType { ... }
// Shorthand aliases work too:
#[sync_to(ts, py, gql)] // ts=typescript, py=python, gql=graphql
}
Supported languages:
typescript/tspython/pygo/golangswiftkotlin/ktgraphql/gqljson_schema/jsonschema
#[tw(...)]
Fine-tune the generated output:
#[tw(skip)]
Exclude a field from generated output:
#![allow(unused)]
fn main() {
pub struct User {
pub id: String,
#[tw(skip)] // Not included in generated types
pub password_hash: String,
}
}
#[tw(rename = "...")]
Override the field/variant name in output:
#![allow(unused)]
fn main() {
pub struct User {
#[tw(rename = "displayName")]
pub username: String,
}
}
#[tw(optional)]
Force a field to be optional:
#![allow(unused)]
fn main() {
pub struct Config {
#[tw(optional)] // Generated as optional even if not Option<T>
pub timeout: u32,
}
}
#[tw(type = "...")]
Override the generated type string:
#![allow(unused)]
fn main() {
pub struct Custom {
#[tw(type = "Record<string, number>")]
pub metrics: serde_json::Value,
}
}
#[tw(zod)] / #[tw(zod = false)]
Control Zod schema generation (TypeScript only):
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(typescript)]
#[tw(zod)] // Enable Zod (default for TypeScript)
pub struct WithZod { ... }
#[derive(TypeWriter)]
#[sync_to(typescript)]
#[tw(zod = false)] // Disable Zod
pub struct NoZod { ... }
}
Disable globally in typewriter.toml:
[typescript]
zod = false
Serde Attributes
typewriter automatically reads serde attributes:
#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize, TypeWriter)]
#[serde(rename_all = "camelCase")]
pub enum Status {
#[serde(rename = "in_progress")]
InProgress,
Completed,
}
}
Type Mappings
Complete reference for Rust → target language type mappings.
Primitive Types
| Rust Type | TypeScript | Python | Go | Swift | Kotlin | GraphQL | JSON Schema |
|---|---|---|---|---|---|---|---|
String | string | str | string | String | String | String | string |
bool | boolean | bool | bool | Bool | Boolean | Boolean | boolean |
u8 | number | int | uint8 | UInt8 | UByte | Int | integer |
u16 | number | int | uint16 | UInt16 | UShort | Int | integer |
u32 | number | int | uint32 | UInt32 | UInt | Int | integer |
u64 | bigint | int | uint64 | UInt64 | ULong | String | integer |
i8 | number | int | int8 | Int8 | Byte | Int | integer |
i16 | number | int | int16 | Int16 | Short | Int | integer |
i32 | number | int | int32 | Int32 | Int | Int | integer |
i64 | bigint | int | int64 | Int64 | Long | String | integer |
f32 | number | float | float32 | Float | Float | Float | number |
f64 | number | float | float64 | Double | Double | Float | number |
Special Types
| Rust Type | TypeScript | Python | Go | Swift | Kotlin | GraphQL | JSON Schema |
|---|---|---|---|---|---|---|---|
Uuid | string | UUID | string | UUID | String | ID | string (uuid) |
DateTime<Utc> | string | datetime | time.Time | Date | Instant | DateTime | string (date-time) |
NaiveDate | string | date | N/A | N/A | N/A | N/A | string (date) |
serde_json::Value | unknown | Any | interface{} | Any | JsonElement | JSON | {} |
Container Types
| Rust Type | TypeScript | Python | Go | Swift | Kotlin | GraphQL | JSON Schema |
|---|---|---|---|---|---|---|---|
Option<T> | T | undefined | Optional[T] = None | *T (omitempty) | T? | T? = null | Nullable | Not in required |
Vec<T> | T[] | list[T] | []T | [T] | List<T> | [T!] | array |
HashMap<K,V> | Record<K, V> | dict[K, V] | map[K]V | [K: V] | Map<K, V> | JSON | object |
(A, B, ...) | N/A | Tuple[A, B, ...] | N/A | N/A | N/A | N/A | prefixItems |
Custom Types
Custom structs and enums are referenced by name in all languages.
Serde Compatibility
typewriter automatically reads and respects serde attributes.
Field Renaming
#![allow(unused)]
fn main() {
#[serde(rename = "userId")]
pub id: String,
}
Field Skip
#![allow(unused)]
fn main() {
#[serde(skip)]
pub internal_field: String,
}
Tagged Enums
Internally Tagged (tag = "type")
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[serde(tag = "type")]
pub enum Event {
Click { x: u32, y: u32 },
KeyPress(char),
}
}
TypeScript:
type Event =
| { type: "Click"; x: number; y: number }
| { type: "KeyPress"; value: string };
Adjacently Tagged
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[serde(tag = "event", content = "data")]
pub enum Event {
Click { x: u32, y: u32 },
}
}
TypeScript:
type Event =
| { event: "Click"; data: { x: number; y: number } };
Untagged
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[serde(untagged)]
pub enum Event {
Click { x: u32, y: u32 },
String(String),
}
}
TypeScript:
type Event = { x: number; y: number } | string;
Field Flatten
#![allow(unused)]
fn main() {
#[serde(flatten)]
pub extra: ExtraData,
}
Rename All Variants
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[serde(rename_all = "snake_case")]
pub enum Status {
InProgress,
UserId,
}
}
Generates: IN_PROGRESS, USER_ID (in appropriate casing for each language).
CLI Commands
Installation
cargo install typebridge-cli
Commands
typewriter generate
Generate type files from Rust source definitions.
# Generate from a single file
typewriter generate src/models.rs
# Generate from all Rust files
typewriter generate --all
# Generate only specific languages
typewriter generate --all --lang typescript,python
# Show unified diffs for changed files
typewriter generate --all --diff
typewriter check
Check if generated files are in sync with Rust source.
# Check all types
typewriter check
# Exit non-zero on drift (for CI)
typewriter check --ci
# Output as JSON
typewriter check --json
# Write JSON report to file
typewriter check --json-out drift-report.json
# Check specific languages
typewriter check --lang typescript,python
typewriter watch
Watch Rust files and regenerate on save.
# Watch src directory
typewriter watch
# Watch custom path
typewriter watch src/models/
# Specific languages
typewriter watch --lang typescript,python
# Adjust debounce interval (ms)
typewriter watch --debounce-ms 100
Cargo Plugin
After installing, use via cargo:
cargo typewriter generate --all
cargo typewriter check --ci
cargo typewriter watch
Exit Codes
| Code | Meaning |
|---|---|
| 0 | Success (no drift for check --ci) |
| 1 | Error or drift detected |
Generics
typewriter supports generic type parameters.
Basic Generics
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(typescript, python)]
pub struct Pagination<T> {
pub items: Vec<T>,
pub total: u64,
pub page: u32,
pub page_size: u32,
}
}
TypeScript:
export interface Pagination<T> {
items: T[];
total: bigint;
page: number;
page_size: number;
}
Python:
from typing import Generic, TypeVar, Optional
T = TypeVar("T")
class Pagination(BaseModel, Generic[T]):
items: list[T]
total: int
page: int
page_size: int
Multiple Type Parameters
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(typescript)]
pub struct Map<K, V> {
pub keys: Vec<K>,
pub values: Vec<V>,
}
}
Nested Generics
Generics can contain other generic types:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(typescript)]
pub struct Nested {
pub users: Vec<Pagination<User>>,
pub configs: HashMap<String, Vec<Config>>,
}
}
Constraints
Generic constraints (e.g., where T: Clone) are not currently preserved in output.
Enums and Unions
typewriter fully supports Rust enums in all their forms.
Unit Enums
Simple enums with no data:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(typescript, python, go)]
pub enum Role {
Admin,
User,
Guest,
}
}
TypeScript:
export type Role = "Admin" | "User" | "Guest";
Python:
class Role(str, Enum):
ADMIN = "Admin"
USER = "User"
GUEST = "Guest"
Tuple Variants
Enums with unnamed fields:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(typescript)]
pub enum Result {
Ok(String),
Err { message: String },
}
}
TypeScript:
type Result =
| { kind: "Ok"; value: string }
| { kind: "Err"; message: string };
Struct Variants
Enums with named fields:
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[serde(tag = "type")]
#[sync_to(typescript)]
pub enum Event {
Click { x: u32, y: u32 },
KeyPress { key: String },
}
}
TypeScript:
type Event =
| { type: "Click"; x: number; y: number }
| { type: "KeyPress"; key: string };
Mixed Variants
#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[serde(tag = "type")]
#[sync_to(typescript)]
pub enum Message {
Text { content: String },
Data(Vec<u8>),
Empty,
}
}
TypeScript:
type Message =
| { type: "Text"; content: string }
| { type: "Data"; value: string }
| { type: "Empty" };
Cross-File Imports
When a struct references another custom type, imports are auto-generated.
TypeScript
#![allow(unused)]
fn main() {
pub struct User {
pub id: String,
}
pub struct UserProfile {
pub user: User,
pub bio: String,
}
}
Generated user-profile.ts:
import type { User } from './user';
export interface UserProfile {
user: User;
bio: string;
}
Python
Generated user_profile.py:
from .user import User
class UserProfile(BaseModel):
user: User
bio: str
Go
Go doesn’t need cross-file imports as long as all types are in the same package.
Deep Nesting
Imports work with deeply nested types:
#![allow(unused)]
fn main() {
pub struct Pagination<T> {
pub items: Vec<T>,
pub total: u64,
}
pub struct UserList {
pub data: Pagination<User>,
}
}
Both files get appropriate imports.
Contributing
Getting Started
- Fork the repository
- Clone your fork
- Create a branch for your change
git clone https://github.com/<your-username>/typewriter.git
cd typewriter
git checkout -b feature/my-feature
Development Setup
Prerequisites
- Rust stable toolchain (1.70+)
cargo-instafor snapshot testing
Build & Test
# Build all crates
cargo build --workspace
# Run all tests
cargo test --workspace
# Run tests and accept new snapshots
cargo insta test --accept --workspace
# Check formatting
cargo fmt --all -- --check
# Run clippy
cargo clippy --workspace -- -D warnings
Project Structure
typewriter/
├── typewriter-core/ # IR types, TypeMapper trait, config
├── typewriter-engine/ # Parser, emitter, drift detection
├── typewriter-macros/ # #[derive(TypeWriter)] proc macro
├── typewriter-typescript/ # TypeScript emitter
├── typewriter-python/ # Python emitter
├── typewriter-go/ # Go emitter
├── typewriter-swift/ # Swift emitter
├── typewriter-kotlin/ # Kotlin emitter
├── typewriter-graphql/ # GraphQL SDL emitter
├── typewriter-json-schema/ # JSON Schema emitter
├── typewriter-cli/ # CLI tools
├── typewriter/ # Main crate (typebridge)
└── typewriter-test/ # Snapshot tests
Adding a New Language Emitter
- Create a new crate
typewriter-<lang> - Implement the
TypeMappertrait - Add to workspace and feature flags
- Add tests and snapshots
See the main CONTRIBUTING.md for detailed instructions.
Commit Messages
Use conventional commits:
feat: add Swift emitter
fix: handle Option<Option<T>> correctly
docs: add Go emitter guide
test: add snapshot tests for enums
Pull Request Process
- Ensure all tests pass
- Format your code
- Run clippy
- Update documentation
- Add changelog entry
- Open PR with description