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
  • Rust is not required for end users who install the released typebridge CLI binary.

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:

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/aarambh-darshan/typewriter/releases/latest/download/typebridge-installer.sh | sh

Windows:

powershell -ExecutionPolicy Bypass -c "irm https://github.com/aarambh-darshan/typewriter/releases/latest/download/typebridge-installer.ps1 | iex"

Rust users can also install from crates.io:

cargo install typebridge-cli

Use typebridge as the primary command. typewriter remains available as a compatibility alias.

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

Ruby (Plugin)

The Ruby emitter generates Sorbet-compatible .rbi type signature files.

Note: This is a plugin emitter. It must be built and installed as a shared library before use.

Quick Start

#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(ruby)]
pub struct User {
    pub id: String,
    pub name: String,
    pub age: Option<u32>,
}
}

Output (user.rbi):

# typed: strict
# Auto-generated by typewriter v0.5.2. DO NOT EDIT.

class User < T::Struct
  const :id, String
  const :name, String
  const :age, T.nilable(Integer), default: nil
end

Type Mappings

RustRuby/Sorbet
StringString
boolT::Boolean
u32, i64, etc.Integer
f64Float
Option<T>T.nilable(T)
Vec<T>T::Array[T]
HashMap<K, V>T::Hash[K, V]
UuidString
DateTimeTime

Enum Mapping

  • Unit enumsT::Enum with enums do block
  • Complex enums → Sealed module with T::Struct subclasses

Configuration

[ruby]
output_dir = "./generated/ruby"
file_style = "snake_case"

See the full Ruby Emitter documentation for more details.

PHP (Plugin)

The PHP emitter generates PHP 8.1+ readonly classes with constructor-promoted properties.

Note: This is a plugin emitter. It must be built and installed as a shared library before use.

Quick Start

#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(php)]
pub struct User {
    pub id: String,
    pub name: String,
    pub age: Option<u32>,
}
}

Output (User.php):

<?php
declare(strict_types=1);

readonly class User
{
    public function __construct(
        public string $id,
        public string $name,
        public ?int $age = null,
    ) {}
}

Type Mappings

RustPHP
Stringstring
boolbool
u32, i64, etc.int
f64float
Option<T>?T
Vec<T>array
HashMap<K, V>array
Uuidstring
DateTime\DateTimeInterface

Enum Mapping

  • Unit enumsenum Role: string (PHP 8.1 backed enum)
  • Complex enumsinterface + readonly class implementations

Configuration

[php]
output_dir = "./generated/php"
file_style = "PascalCase"

See the full PHP Emitter documentation for more details.

Dart/Flutter (Plugin)

The Dart emitter generates json_serializable-compatible Dart classes for Flutter projects.

Note: This is a plugin emitter. It must be built and installed as a shared library before use.

Quick Start

#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(dart)]
pub struct User {
    pub id: String,
    pub user_name: String,
    pub age: Option<u32>,
}
}

Output (user.dart):

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final String id;
  @JsonKey(name: 'user_name')
  final String userName;
  final int? age;

  const User({
    required this.id,
    required this.userName,
    this.age,
  });

  factory User.fromJson(Map<String, dynamic> json) =>
      _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

Type Mappings

RustDart
StringString
boolbool
u32, i64, etc.int
f64double
Option<T>T?
Vec<T>List<T>
HashMap<K, V>Map<K, V>
UuidString
DateTimeDateTime

Enum Mapping

  • Unit enumsenum with @JsonValue() annotations
  • Complex enumssealed class hierarchy (Dart 3.0+)

Flutter Integration

After generating types, run build_runner to generate the *.g.dart files:

dart run build_runner build

Configuration

[dart]
output_dir = "./generated/dart"
file_style = "snake_case"

See the full Dart Emitter documentation for more details.

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

The primary CLI is typebridge. The typewriter binary is kept as a compatibility alias, and cargo typewriter remains available via cargo-typewriter.

Installation

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/aarambh-darshan/typewriter/releases/latest/download/typebridge-installer.sh | sh
powershell -ExecutionPolicy Bypass -c "irm https://github.com/aarambh-darshan/typewriter/releases/latest/download/typebridge-installer.ps1 | iex"

Global Options

  • --config <PATH>
  • --format text|json
  • --verbose
  • --dry-run
  • --version

Commands

typebridge generate

typebridge generate src/models.rs
typebridge generate --all
typebridge generate --all --lang typescript,python
typebridge generate --all --diff

typebridge check

typebridge check
typebridge check --ci
typebridge check --json
typebridge check --json-out drift-report.json
typebridge check --lang typescript,python

typebridge watch

typebridge watch
typebridge watch src/models/
typebridge watch --lang typescript,python
typebridge watch --debounce-ms 100

typebridge init

typebridge init
typebridge init --force
typebridge --dry-run init

typebridge doctor

typebridge doctor
typebridge --format json doctor

typebridge plugin

Plugin commands are experimental in v1.0.0:

typebridge plugin list
typebridge plugin validate ./target/release/libtypewriter_plugin_ruby.so
typebridge plugin info ruby

Cargo Plugin

cargo typewriter generate --all
cargo typewriter check --ci
cargo typewriter watch

Exit Codes

CodeMeaning
0Success, help/version output, or no drift for check --ci
1Runtime error or drift detected
2Invalid CLI usage

Plugin System Overview

typewriter v0.5.2 introduced a dynamic plugin architecture. In v1.0.0 this remains experimental: local plugin loading is available, but there is no plugin add command, registry marketplace, or stable community plugin ABI.

How It Works

  1. Plugin authors implement the EmitterPlugin trait from the typewriter-plugin crate
  2. Plugins are compiled as shared libraries (.so / .dylib / .dll)
  3. The CLI dynamically loads plugins at startup via libloading
  4. Plugins provide TypeMapper implementations just like built-in emitters

Architecture

┌─────────────────┐       ┌──────────────────┐
│  typebridge CLI │──────▶│  PluginRegistry   │
└─────────────────┘       └────────┬─────────┘
                                   │
                    ┌──────────────┼──────────────┐
                    ▼              ▼              ▼
            ┌──────────┐  ┌──────────┐  ┌──────────┐
            │   Ruby   │  │   PHP    │  │   Dart   │
            │  .so/.dll│  │  .so/.dll│  │  .so/.dll│
            └──────────┘  └──────────┘  └──────────┘

Key Components

ComponentCratePurpose
EmitterPlugin traittypewriter-pluginInterface for plugin implementations
declare_plugin! macrotypewriter-pluginGenerates C ABI entry points
PluginRegistrytypewriter-engineLoads and manages plugins
PluginConfigtypewriter-pluginPlugin-specific config from TOML

Bundled Plugins

PluginLanguage IDExtensionDescription
typewriter-plugin-rubyruby.rbiSorbet type signatures
typewriter-plugin-phpphp.phpPHP 8.1+ readonly classes
typewriter-plugin-dartdart.dartjson_serializable classes

Limitations

  • CLI only — plugins are loaded at CLI startup, not during cargo build proc-macro expansion
  • No hot reload — plugins are loaded once; restart CLI after changes
  • ABI versioning — plugins must match PLUGIN_API_VERSION, and the ABI is not stable in v1.0.0

Writing a Plugin

This guide walks you through creating a custom typewriter plugin.

1. Create a Crate

cargo new --lib typewriter-plugin-mylang

2. Cargo.toml

[package]
name = "typewriter-plugin-mylang"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
typewriter-plugin = "0.1.0"
typewriter-core = "0.5.2"

The cdylib crate type is required for dynamic loading.

3. Implement TypeMapper

#![allow(unused)]
fn main() {
use typewriter_plugin::prelude::*;

struct MyLangMapper;

impl TypeMapper for MyLangMapper {
    fn map_primitive(&self, ty: &PrimitiveType) -> String {
        match ty {
            PrimitiveType::String => "string".into(),
            PrimitiveType::Bool => "boolean".into(),
            PrimitiveType::U32 => "uint32".into(),
            // ... implement all primitives
            _ => "any".into(),
        }
    }

    fn map_option(&self, inner: &TypeKind) -> String {
        format!("{}?", self.map_type(inner))
    }

    fn map_vec(&self, inner: &TypeKind) -> String {
        format!("List<{}>", self.map_type(inner))
    }

    fn map_hashmap(&self, key: &TypeKind, value: &TypeKind) -> String {
        format!("Map<{}, {}>", self.map_type(key), self.map_type(value))
    }

    fn map_tuple(&self, elements: &[TypeKind]) -> String {
        let inner: Vec<String> = elements.iter().map(|e| self.map_type(e)).collect();
        format!("({})", inner.join(", "))
    }

    fn map_named(&self, name: &str) -> String { name.into() }

    fn emit_struct(&self, def: &StructDef) -> String {
        // Your struct rendering logic
        todo!()
    }

    fn emit_enum(&self, def: &EnumDef) -> String {
        // Your enum rendering logic
        todo!()
    }

    fn file_header(&self, type_name: &str) -> String {
        format!("// Generated by typewriter. Source: {}\n\n", type_name)
    }

    fn file_extension(&self) -> &str { "mylang" }

    fn file_naming(&self, type_name: &str) -> String {
        to_file_style(type_name, FileStyle::SnakeCase)
    }
}
}

4. Implement EmitterPlugin

#![allow(unused)]
fn main() {
struct MyLangPlugin;

impl MyLangPlugin {
    fn new() -> Self { Self }
}

impl EmitterPlugin for MyLangPlugin {
    fn language_id(&self) -> &str { "mylang" }
    fn language_name(&self) -> &str { "My Language" }
    fn version(&self) -> &str { "0.1.0" }
    fn default_output_dir(&self) -> &str { "./generated/mylang" }
    fn file_extension(&self) -> &str { "mylang" }

    fn mapper(&self, _config: &PluginConfig) -> Box<dyn TypeMapper> {
        Box::new(MyLangMapper)
    }
}

declare_plugin!(MyLangPlugin);
}

5. Build and Install

cargo build --release
cp target/release/libtypewriter_plugin_mylang.so ~/.typewriter/plugins/

6. Validate

typebridge plugin validate ./target/release/libtypewriter_plugin_mylang.so

7. Use

#![allow(unused)]
fn main() {
#[derive(TypeWriter)]
#[sync_to(mylang)]
pub struct User { ... }
}
typebridge generate --all

Plugin Configuration

Plugins are configured through typewriter.toml.

Plugin Discovery

[plugins]
# Directory to scan for .so/.dylib/.dll files
dir = "~/.typewriter/plugins/"

# Explicit paths to plugin libraries
paths = [
    "./my-plugins/libtypewriter_plugin_ruby.so",
]

Plugin-Specific Settings

Each plugin can have its own TOML section using its language_id as the key:

[ruby]
output_dir = "./generated/ruby"
file_style = "snake_case"

[php]
output_dir = "./generated/php"
file_style = "PascalCase"

[dart]
output_dir = "./generated/dart"
file_style = "snake_case"

Standard Keys

All plugins support these standard keys:

KeyTypeDescription
output_dirStringOutput directory for generated files
file_styleStringFile naming style: snake_case, kebab-case, PascalCase

Additional keys are passed to the plugin via PluginConfig.extra.

Plugin CLI Commands

Plugin commands are experimental in v1.0.0. They support local plugin inspection and validation only; there is no plugin add command or registry.

typebridge plugin list

typebridge plugin list
typebridge --format json plugin list

typebridge plugin validate <path>

typebridge plugin validate ./target/release/libtypewriter_plugin_ruby.so

typebridge plugin info <name>

typebridge plugin info ruby
typebridge --format json plugin info ruby

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