Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

Resources

Installation

Prerequisites

  • Rust stable toolchain (1.70+)
  • A Cargo project with serde for 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

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

PartPurpose
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

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

StyleExample
kebab-caseuser-profile.ts
snake_caseuser_profile.py
PascalCaseUserProfile.swift

Default Values

If not specified:

SettingDefault
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 zodtrue
Python pydantic_v2true
Go package_nametypes
Kotlin package_nametypes

TypeScript

Generates TypeScript interfaces and optionally Zod validation schemas.

Type Mappings

Rust TypeTypeScript Type
Stringstring
boolboolean
u8, u16, i8, i16number
u32, i32, f32, f64number
u64, i64, u128, i128bigint
Uuidstring
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: UserProfileuser-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 TypePython Type
Stringstr
boolbool
u8, u16, u32, u64, i8, i16, i32, i64int
f32, f64float
UuidUUID
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: UserProfileuser_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 TypeGo Type
Stringstring
boolbool
u8, u16, u32uint8, uint16, uint32
u64uint64
i8, i16, i32, i64int8, int16, int32, int64
f32, f64float32, float64
Uuidstring
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: UserProfileuser_profile.go

[go]
file_style = "PascalCase"  # UserProfile.go

Swift

Generates Swift structs with Codable protocol.

Type Mappings

Rust TypeSwift Type
StringString
boolBool
u8, u16, u32, u64UInt8, UInt16, UInt32, UInt64
i8, i16, i32, i64Int8, Int16, Int32, Int64
f32, f64Float, Double
UuidUUID
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: UserProfileUserProfile.swift

[swift]
file_style = "snake_case"  # user_profile.swift

Kotlin

Generates Kotlin data classes with kotlinx.serialization.

Type Mappings

Rust TypeKotlin Type
StringString
boolBoolean
u8, u16, u32, u64UByte, UShort, UInt, ULong
i8, i16, i32, i64Byte, Short, Int, Long
f32, f64Float, Double
UuidString
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: UserProfileUserProfile.kt

[kotlin]
file_style = "snake_case"  # user_profile.kt

GraphQL

Generates GraphQL Schema Definition Language (SDL) types.

Type Mappings

Rust TypeGraphQL Type
StringString
boolBoolean
u8, u16, u32, i8, i16, i32Int
f32, f64Float
UuidID
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: UserProfileuser_profile.graphql

[graphql]
file_style = "kebab-case"  # user-profile.graphql

JSON Schema

Generates JSON Schema Draft 2020-12 definitions.

Type Mappings

Rust TypeJSON 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: UserProfileuser_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 / ts
  • python / py
  • go / golang
  • swift
  • kotlin / kt
  • graphql / gql
  • json_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 TypeTypeScriptPythonGoSwiftKotlinGraphQLJSON Schema
StringstringstrstringStringStringStringstring
boolbooleanboolboolBoolBooleanBooleanboolean
u8numberintuint8UInt8UByteIntinteger
u16numberintuint16UInt16UShortIntinteger
u32numberintuint32UInt32UIntIntinteger
u64bigintintuint64UInt64ULongStringinteger
i8numberintint8Int8ByteIntinteger
i16numberintint16Int16ShortIntinteger
i32numberintint32Int32IntIntinteger
i64bigintintint64Int64LongStringinteger
f32numberfloatfloat32FloatFloatFloatnumber
f64numberfloatfloat64DoubleDoubleFloatnumber

Special Types

Rust TypeTypeScriptPythonGoSwiftKotlinGraphQLJSON Schema
UuidstringUUIDstringUUIDStringIDstring (uuid)
DateTime<Utc>stringdatetimetime.TimeDateInstantDateTimestring (date-time)
NaiveDatestringdateN/AN/AN/AN/Astring (date)
serde_json::ValueunknownAnyinterface{}AnyJsonElementJSON{}

Container Types

Rust TypeTypeScriptPythonGoSwiftKotlinGraphQLJSON Schema
Option<T>T | undefinedOptional[T] = None*T (omitempty)T?T? = nullNullableNot 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>JSONobject
(A, B, ...)N/ATuple[A, B, ...]N/AN/AN/AN/AprefixItems

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

CodeMeaning
0Success (no drift for check --ci)
1Error 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

  1. Fork the repository
  2. Clone your fork
  3. 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-insta for 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

  1. Create a new crate typewriter-<lang>
  2. Implement the TypeMapper trait
  3. Add to workspace and feature flags
  4. 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

  1. Ensure all tests pass
  2. Format your code
  3. Run clippy
  4. Update documentation
  5. Add changelog entry
  6. Open PR with description