How to Build a Rust CLI Tool to Generate Typescript Types from Rust
Tags: rust, typescript, tutorial, opensource
Published: 2022-10-18 13:27:13

# Table of Contents

1. [Motivation](#motivation)
1. [Introduction](#introduction)
1. [Project Setup](#project-setup)
1. [Building the CLI](#building-the-cli)
1. [Working with Files](#working-with-files)
1. [Basic Types](#basic-types)
1. [Enums](#enums)
1. [Structs](#structs)
1. [Tuples](#tuples)
1. [Standard Library Types](#standard-library-types)
1. [Tests](#tests)
1. [Building and Publishing](#building-and-publishing)
1. [Future Improvements](#future-improvements)
1. [Similar Open Source Tools](#similar-open-source-tools)
1. [Conclusion](#conclusion)

# Motivation

Why would you want to write a tool that converts Rust types to Typescript types? Aren't they entirely separate languages?

There are many reasons you might want to do so. The most common (though far from the only) case that we'll be focusing on is someone who is writing an API (like a web server) in Rust and consuming it with a Typescript frontend.

Being able to automate the sharing types defined on the backend with the frontend has a lot of benefits and and can significantly reduce development time spent re-writing the same definitions in multiple languages.

# Introduction

Our very first goal will be to turn this into valid Typescript code by writing the type definitions in Rust and using our program to convert them into Typescript types:

```ts
import { NumberAlias } from "./types";

const printNum = (num: NumberAlias) => {
  console.log(num);
};

printNum(10);
```

If you try and paste this code into a `.ts` file and run it you will get an error saying `NumberAlias` is undefined and the `types` file you are trying to import it from does not exist. We will use the output of our program to create that file.

Type aliases work almost identically in Rust as they do in TS so this first challenge will be very simple in terms of output. Our goal is to generate the file:

`types.d.ts`

```ts
type NumberAlias = number;
```

From this Rust input file:

`types.rs`

```rust
type NumberAlias = i32;
```

# Project Setup

There are two crates we will be relying on to develop out type conversion utility:

- [clap](https://crates.io/crates/clap) short for _Command Line Argument Parser_ is the most popular Rust crate for dealing with command line arguments. We'll be using it to allow our uses to tell us the input/out filenames of the Rust and Typescript files they are using,

- [syn](https://crates.io/crates/syn) derived from the word _syntax_ is a crate designed to convert Rust source code into a type-safe syntax tree where each token can be examined and parsed individually.

Open up your favourite terminal. Make sure you have [installed Rust on your system](https://www.rust-lang.org/tools/install). Fresh installations will also include Rust's package manager called [Cargo](https://doc.rust-lang.org/cargo/) which we'll need for this tutorial.

When you're ready run the following command to create the utility. I've decided to call mine `typester`, but you can call yours whatever you like.

```bash
cargo new typester
```

This will create a `Cargo.toml` file (equivalent to `package.json` in the JS/TS world) and an `src` directory with a `main.rs` file with a demo program. Test it out with `cargo run` and you should see "Hello, world!" printed to your terminal.

Next we will add the two packages (crates) that we need to write our program. Run the following command to automatically add the latest versions to your `Cargo.toml` file:

```
cargo add clap
cargo add syn --features=full,extra-traits
```

The `syn` crate does not include all features by default, so make sure you include the ones I've listed above. Full gives us access to the `parse_file` function and extra-traits derives `Debug` for syn's types to make it easier for us to log values and debug our code.

After adding your `Cargo.toml` file should include the following:

```toml
[dependencies]
clap = "4.0.9"
syn = { version = "1.0.101", features = ["full", "extra-traits"] }
```

Your version numbers may not match exactly, though if you are seeing different major versions I would recommend you align with the versions shown above for the sake of this tutorial in case either library has introduced breaking API changes since it was written.

# Building the CLI

Before diving into the parsing itself, let's take a moment to set up the command line argument parsing with the `clap` crate. Fortunately for us, clap makes that extremely easy.

Here's some sample code. You can review clap's [documentation](https://docs.rs/clap/4.0.9/clap/index.html) for the version we are using, or read ahead for a brief overview.

```rust
use clap::{Arg, Command};

fn main() {
     let matches = Command::new("Typester")
        .version("0.1.0")
        .author("Alex E")
        .about("Convert Rust types to Typescript types")
        .arg(
            Arg::new("input")
                .short('i')
                .long("input")
                .required(true)
                .help("The Rust file to process (including extension)"),
        )
        .arg(
            Arg::new("output")
                .short('o')
                .long("output")
                .required(true)
                .help("The name of the Typescript file to output (including extension)"),
        )
        .get_matches();

    let input_filename = matches.get_one::<String>("input").expect("input required");
    let output_filename = matches
        .get_one::<String>("output")
        .expect("output required");

    dbg!(input_filename);
    dbg!(output_filename);
}
```

The above code will provide you with a great baseline to get started. It forces the user to define both a input filename (Rust) and an output filename (Typescript).

`clap` takes care of some really handy grunt work like providing a nicely formatted `--help` output and enforcing required arguments.

Before testing the program out, it's good to understand a Rust CLI's (command line interface) argument structure. When compiling a release binary you can simply add the arguments as any other CLI after the name of the program, but for `cargo run` you need to separate the `run` and arguments with two hyphens like so:

```bash
cargo run -- --input=src/types.rs --output=types.d.ts
```

The output of this command with our program will be:

```
[src/main.rs:29] input_filename = "src/types.rs"
[src/main.rs:30] output_filename = "types.d.ts"
```

Furthermore, as a user who may not be aware of the required arguments can take advantage of the `--help` flag:

```
cargo run -- --help
```

```
Convert Rust types to Typescript types

Usage: typester --input <input> --output <output>

Options:
  -i, --input <input>    The Rust file to process (including extension)
  -o, --output <output>  The name of the Typescript file to output (including extension)
  -h, --help             Print help information
  -V, --version          Print version information
```

That nicely formatted help output is thanks to the great `clap` crate.

# Working With Files

We'll use the standard library's `File`, `Read` and `Path` to read the contents of our Rust file, and then parse it into a `syn::File` where we can iterate over the contents of the file in a typesafe manner.

```rust
use clap::{Arg, Command};
use std::{
    fs::File,
    io::{Read, Write},
    path::Path,
};

fn main() {

     ... // Code from previous section

    let input_path = Path::new(input_filename);

    let mut input_file =
        File::open(input_path).expect(&format!("Unable to open file {}", input_path.display()));

    let mut input_file_text = String::new();

    input_file
        .read_to_string(&mut input_file_text)
        .expect("Unable to read file");

    // This is our tokenized version of Rust file ready to process
    let input_syntax: syn::File = syn::parse_file(&input_file_text).expect("Unable to parse file");
}
```

Since this is a simple utility meant for learning and does not need to be bulletproof and handle every potential bad file input, we'll simply use `.expect()` in each scenario that doesn't conform to our requirements.

For those unfamiliar, `.expect()` is the same as `.unwrap()` which will `panic` on a `Result` of `Err` or an `Option` of `None`, but it allows us to provide some information to the user as to where/why that failure occurred.

Now that we have created a `syn:File` which the contents of our Rust file ready to parse, we can iterate over each token one at a time and start converting them into strings of text that conform to Typescript's syntax rules.

To do this we take our syn file and iterate over it with `.items.iter()`:

```rust
use clap::{Arg, Command};
use std::{
    fs::File,
    io::{Read, Write},
    path::Path,
};

fn main() {

    ... // Code from previous sections

    // This string will store the output of the Typescript file that we will
    // continuously append to as we process the Rust file
    let mut output_text = String::new();

    for item in input_syntax.items.iter() {
        match item {
            // This `Item::Type` enum variant matches our type alias
            syn::Item::Type(item_type) => {
                let type_text = parse_item_type(item_type);
                output_text.push_str(&type_text);
            }
            _ => {
                dbg!("Encountered an unimplemented type");
            }
        }
    }

    let mut output_file = File::create(output_filename).unwrap();

    write!(output_file, "{}", output_text).expect("Failed to write to output file");
}

fn parse_item_type(item_type: &syn::ItemType) -> String {
    String::from("todo")
}
```

At this point we are ready to test it out Create the following `types.rs` file directly next to your `main.rs` file:

`types.rs`

```rust
type NumberAlias = i32;
```

Now run the following command:

```
cargo run -- --input=src/types.rs --output=types.d.ts
```

_(Note the `"--"` before the input flag, this is a placeholder for the program name while we are building in development, make sure you include it or your program will not work properly.)_

In your root of your project you should see a `types.d.ts` file appear containing the text `todo`:

`types.d.ts`

```ts
todo;
```

# Basic Types

Now we are ready to parse the actual tokens. We'll start with the simplest one, the basic type alias `type NumberAlias = i32;`.

The will match on `syn::Item::Type` as we have written above and call our `parse_item_type` function which we need to expand and define. Each of our parsing functions for the different types our crate handles will return a string so we can simply concatenate them all together to build the output.

```rust
... // Code from previous sections with `parse_item_type` stub removed

/// Converts a Rust item type to a Typescript type
///
/// ## Examples
///
/// **Input:** type NumberAlias = i32;
///
/// **Output:** export type NumberAlias = number;
fn parse_item_type(item_type: &syn::ItemType) -> String {
    let mut output_text = String::new();

    output_text.push_str("export type ");

    // `ident` is the name of the type alias, `NumberAlias` from the example
    output_text.push_str(&item_type.ident.to_string());
    output_text.push_str(" = ");

    let type_string = parse_type(&item_type.ty);
    output_text.push_str(&type_string);
    output_text.push_str(";");

    output_text
}
```

The `NumberAlias` in our type will be captured with the `ident` value.

The `i32` portion however could be any number of type values, and not necessarily a primitive. It would be `(i32, i32)` for example.

For this we'll create another handler function called `parse_type`:

```rust
/// Converts a Rust type into a Typescript type
///
/// ## Examples
///
/// **Input:** (i32, i32) / Option<String>
///
/// **Output:** \[number, number\] / Option<string>;
fn parse_type(syn_type: &syn::Type) -> String {
    let mut output_text = String::new();

    match syn_type {
        // Primitive types like i32 will match Path
        // We currently do not do anything with full paths
        // so we take only the last() segment (the type name)
        syn::Type::Path(type_path) => {
            let segment = type_path.path.segments.last().unwrap();

            let field_type = segment.ident.to_string();

            let ts_field_type = parse_type_ident(&field_type).to_owned();
            output_text.push_str(&ts_field_type);

            match &segment.arguments {
                // A simple type like i32 matches here as it
                // does not include any arguments
                syn::PathArguments::None => {}
                _ => {
                    dbg!("Encountered an unimplemented token");
                }
            }
        _ => {
            dbg!("Encountered an unimplemented token");
        }
    };

    output_text
}
```

As you an see above, we are only focusing on our simple type for the time being and ignoring all other matches.

If any type that our utility does not know how to process is encountered, the `dbg!` macro will print a message along with a convenient line number so user's can easily identify which condition was hit and expand on the utility if they choose.

You may also notice that for this `Path` type we have matched we are only taking the `last()` token. That is because currently we are not doing anything with the other parts of the path (for example `std:fs:File`), just extracting the name of the type.

The final function necessary to finish our prototype we will call `parse_type_ident` which deals with the most primitive types, or simply return the name of the type if it is a custom type.

All of Rust's many different types of numbers will simply be treated as a `number` when deserialized as Typescript:

```rust
/// Convert a primitive Rust ident to an equivalent Typescript type name
/// Translate primitive types to Typescript equivalent otherwise
/// returns the ident untouched
///
/// ## Examples
///
/// **Input:** i32 / Option / bool;
///
/// **Output:** number / Option / boolean;
fn parse_type_ident(ident: &str) -> &str {
    match ident {
        "i8" | "i16" | "i32" | "i64" | "i128" | "u8" | "u16" | "u32" | "u64" | "f32" | "f64"
        | "isize" | "usize" => "number",
        "str" | "String" | "char" => "string",
        "bool" => "boolean",
        _ => ident,
    }
}
```

We're finally ready to try it out. Run the command again:

```
cargo run -- --input=src/types.rs --output=types.d.ts
```

With the input:

`types.rs`

```rust
type NumberAlias = i32;
```

We get the output:

`types.d.ts`

```ts
export type NumberAlias = number;
```

And that's a valid Typescript type!

Congratulations, we have our first minimum prototype for a Rust -> TS conversion utility.

Of course as you can imagine, getting complete support for serialization of all Rust's types is a monumental effort, likely requiring months or years of continued development.

But fortunately like many things, 10-20% coverage of the most common types will take care of probably 80-90% of the most common use cases.

So let's focus on getting a few more of the most common cases covered, most specifically: `enums` and `structs`:

# Enums

This is the example `enum` we'll be using to test our support:

```rust
#[serde(tag = "t", content = "c")]
enum Colour {
    Red(i32),
    Green(i32),
    Blue(i32),
}
```

And the expected output we'll be trying to create:

```ts
export type Colour =
  | { t: "Red"; c: number }
  | { t: "Green"; c: number }
  | { t: "Blue"; c: number };
```

You may be wondering what the `#[serde(tag = "t", content = "c")]` attribute is for?

Well it turns out there are quite a few different ways that an enum can be serialized. If you'd like to learn more about them [this page in the serde documentation covers all the standard ones.](https://serde.rs/enum-representations.html)

The method we'll be using is called [adjacently tagged](https://serde.rs/enum-representations.html#adjacently-tagged), the reason for this is that it makes it very clean and easy to handle on the Typescript side by using [union types](https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html) to discriminate on the `t` value and infer the correct type of the content `c` value.

Technically these `t` and `c` values can be any name that you choose, and we could write our utility to parse the attribute and read what the user has set, but since we are focusing on simplicity for now we're going to operate on the assumption that the user is using `t` and `c`.

With that preliminary assumption in place, we can now expand our initial match in the main function to include enums, and create a new `parse_item_enum` function:

```rust
    ... // Main function from previous sections

    for item in input_syntax.items.iter() {
        match item {
            // This `Item::Type` enum variant matches our type alias
            syn::Item::Type(item_type) => {
                let type_text = parse_item_type(item_type);
                output_text.push_str(&type_text);
            }
            syn::Item::Enum(item_enum) => {
                let enum_text = parse_item_enum(item_enum);
                output_text.push_str(&enum_text);
            }
            _ => {
                dbg!("Encountered an unimplemented type");
            }
        }
    }

    let mut output_file = File::create(output_filename).unwrap();

    write!(output_file, "{}", output_text).expect("Failed to write to output file");
}
```

Now we create the `parse_item_enum` function:

```rust

/// Converts a Rust enum to a Typescript type
///
/// ## Examples
///
/// **Input:**
/// enum Colour {
///     Red(i32, i32),
///     Green(i32),
///     Blue(i32),
/// }
///
/// **Output:**
/// export type Colour =
///   | { t: "Red"; c: number }
///   | { t: "Green"; c: number }
///   | { t: "Blue"; c: number };
fn parse_item_enum(item_enum: &syn::ItemEnum) -> String {
    let mut output_text = String::new();

    output_text.push_str("export type");
    output_text.push_str(" ");

    let enum_name = item_enum.ident.to_string();
    output_text.push_str(&enum_name);
    output_text.push_str(" ");
    output_text.push_str("=");
    output_text.push_str(" ");

    for variant in item_enum.variants.iter() {
        // Use the pipe character for union types
        // Typescript also allows it before the first type as valid syntax
        // https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types
        output_text.push_str(" | {");
        output_text.push_str(" ");

        // For simplicity this implementation we are using assumes that enums will be
        // using serde's "Adjacently Tagged" attribute
        // #[serde(tag = "t", content = "c")]
        // https://serde.rs/enum-representations.html#adjacently-tagged
        // As an improvement on this implementation you could parse the attribute
        // and handle the enum differently depending on which attribute the user chose
        output_text.push_str("t: \"");
        let variant_name = variant.ident.to_string();
        output_text.push_str(&variant_name);
        output_text.push_str("\" , c: ");

        match &variant.fields {
            syn::Fields::Named(named_fields) => {
                output_text.push_str("{");
                for field in named_fields.named.iter() {
                    if let Some(ident) = &field.ident {
                        output_text.push_str(&ident.to_string());
                        output_text.push_str(":");

                        let field_type = parse_type(&field.ty);
                        output_text.push_str(&field_type);
                        output_text.push_str(";");
                    }
                }
                output_text.push_str("}");
            }
            syn::Fields::Unnamed(unnamed_fields) => {
                // Currently only support a single unnamed field: e.g the i32 in Blue(i32)
                let unnamed_field = unnamed_fields.unnamed.first().unwrap();
                let field_type = parse_type(&unnamed_field.ty);
                output_text.push_str(&field_type);
            }
            syn::Fields::Unit => {
                output_text.push_str("undefined");
            }
        }

        output_text.push_str("}");
    }
    output_text.push_str(";");

    output_text
}
```
I've included comments above to annotate some of the sections that may be a bit more difficult to follow. 

The great thing is that we are already seeing the benefits of reuse with the previous functions we created like `parse_type`.  

As you continue to developer and improve this utility you'll find that creaking eac individual token parser into its own function allows for a lot of opportunities for reuse when you start handling more complex types.

With this in place we are now ready to test:

Expand our `types.rs` file to include both types we now support:

`types.rs`
```rust
type NumberAlias = i32;

#[serde(tag = "t", content = "c")]
enum Colour {
    Red(i32),
    Green(i32),
    Blue(i32),
}
```

Run the command:

```
cargo run -- --input=src/types.rs --output=types.d.ts
```

And our output will be:

`types.d.ts`
```ts
export type Colour =
  | { t: "Red"; c: number }
  | { t: "Green"; c: number }
  | { t: "Blue"; c: number };

export type NumberAlias = number;
```

_(Note that I am using a Typescript formatter, specifically [prettier](https://prettier.io/), on the output to make it more human friendly.  The actual output, while totally valid TS, will appear on a single line)_

# Structs

Now let's add support for structs.  Our example test input will be:

```rust
struct Person {
    name: String,
    age: u32,
    enjoys_coffee: bool,
}
```

And our expected output will be:

```ts
export interface Person {
  name: string;
  age: number;
  enjoys_coffee: boolean;
}
```

Update the match in the `main` function to match on `syn::Item::Struct`:

```rust
    ... // Main function from previous sections

    for item in input_syntax.items.iter() {
        match item {
            syn::Item::Type(item_type) => {
                let type_text = parse_item_type(item_type);
                output_text.push_str(&type_text);
            }
            syn::Item::Enum(item_enum) => {
                let enum_text = parse_item_enum(item_enum);
                output_text.push_str(&enum_text);
            }
            syn::Item::Struct(item_struct) => {
                let struct_text = parse_item_struct(item_struct);
                output_text.push_str(&struct_text);
            }
            _ => {
                dbg!("Encountered an unimplemented type");
            }
        }
    }

    let mut output_file = File::create(output_filename).unwrap();

    write!(output_file, "{}", output_text).expect("Failed to write to output file");
}
```

Create the `parse_item_struct` function:

```rust
/// Converts a Rust struct to a Typescript interface
///
/// ## Examples
///
/// **Input:**
/// struct Person {
///     name: String,
///     age: u32,
///     enjoys_coffee: bool,
/// }
///
/// **Output:**
/// export interface Person {
///     name: string;
///     age: number;
///     enjoys_coffee: boolean;
/// }
fn parse_item_struct(item_struct: &syn::ItemStruct) -> String {
    let mut output_text = String::new();

    let struct_name = item_struct.ident.to_string();
    output_text.push_str("export interface");
    output_text.push_str(" ");
    output_text.push_str(&struct_name);
    output_text.push_str(" ");
    output_text.push_str("{");
    match &item_struct.fields {
        syn::Fields::Named(named_fields) => {
            for named_field in named_fields.named.iter() {
                match &named_field.ident {
                    Some(ident) => {
                        let field_name = ident.to_string();
                        output_text.push_str(&field_name);
                        output_text.push_str(":");
                    }
                    None => todo!(),
                }
                let field_type = parse_type(&named_field.ty);
                output_text.push_str(&field_type);
                output_text.push_str(";");
            }
        }
        // For tuple structs we will serialize them as interfaces with
        // fields named for the numerical index to align with serde's
        // default handling of this type
        syn::Fields::Unnamed(fields) => {
            // Example: struct Something (i32, Anything);
            // Output: export interface Something { 0: i32, 1: Anything }
            for (index, field) in fields.unnamed.iter().enumerate() {
                output_text.push_str(&index.to_string());
                output_text.push_str(":");
                output_text.push_str(&parse_type(&field.ty));
                output_text.push_str(";");
            }
        }
        syn::Fields::Unit => (),
    }
    output_text.push_str("}");
    output_text.push_str(";");

    output_text
}
```

Let's test it out:

`types.rs`
```rust
type NumberAlias = i32;

#[serde(tag = "t", content = "c")]
enum Colour {
    Red(i32),
    Green(i32),
    Blue(i32),
}

struct Person {
    name: String,
    age: u32,
    enjoys_coffee: bool,
}
```

```
cargo run -- --input=src/types.rs --output=types.d.ts
```

And our output:

`types.d.ts`

```ts
export type NumberAlias = number;

export type Colour =
  | { t: "Red"; c: number }
  | { t: "Green"; c: number }
  | { t: "Blue"; c: number };

export interface Person {
  name: string;
  age: number;
  enjoys_coffee: boolean;
}
```

# Tuples

Tuples are a common data type in Rust that roughly correspond to array types in Typescript where each element has a specific type (so a tuple like `(i32, i32)` for example would be translated to `[number, number]` rather than the less specific `number[]` that vectors would translate into.)

We can add simple support by expanding the `match &segment.arguments` in our `parse_type` function to add this addition match arm:

```rust

// in the `parse_type` function

    ...
    // Tuple types like (i32, i32) will match here
    syn::Type::Tuple(type_tuple) => {
        output_text.push_str("[");
        for elem in type_tuple.elems.iter() {
            output_text.push_str(&parse_type(elem));
            output_text.push_str(",");
        }
        output_text.push_str("]");
    }
    ...
```

# Standard Library Types

As a final update, let's add support for some of Rust's most common types in the standard library like `Option`, and `HashMap`.

The reason for these in particular is that they are common data types to be serialized, for example when parsed on the Javascript/Typescript side `Option<T>` can be thought of as `T | undefined` and `HashMap<T, U>` can be deserialized as an object in the TS form of `Record<T, U>`.

No doubt you can come up with more, but here are some straightforward TS mappings of some of the most common data types. We don't need to do these dynamically, we can simply prepend these pre-written types to the file output before we begin to parse:

Add the following line immediately after the variable `output_text` is declared in the main function:

```rust
output_text.push_str(&create_initial_types());
```

Then create that `create_initial_types` function as follows:

```rust
/// Initialize some Typescript equivalents of
/// core Rust types like Result, Option, etc
fn create_initial_types() -> String {
    let mut output_text = String::new();

    output_text
        .push_str("type HashSet<T extends number | string> = Record<T, undefined>;");
    output_text.push_str("type HashMap<T extends number | string, U> = Record<T, U>;");
    output_text.push_str("type Vec<T> = Array<T>;");
    output_text.push_str("type Option<T> = T | undefined;");
    output_text.push_str("type Result<T, U> = T | U;");

    output_text
}
```

The final version (of our incomplete program 😁) takes the following input:

`types.rs`

```rust
type NumberAlias = i32;

#[serde(tag = "t", content = "c")]
enum Colour {
    Red(i32),
    Green(i32),
    Blue(i32),
}

struct Person {
    name: String,
    age: u32,
    enjoys_coffee: bool,
}

struct ComplexType {
    colour_map: HashMap<String, Colour>,
    list_of_names: Vec<String>,
    optional_person: Option<Person>,
}
```

And produces the following valid Typescript output:

`types.d.ts`

```ts
type HashSet<T extends number | string> = Record<T, undefined>;
type HashMap<T extends number | string, U> = Record<T, U>;
type Vec<T> = Array<T>;
type Option<T> = T | undefined;
type Result<T, U> = T | U;

export type NumberAlias = number;

export type Colour =
  | { t: "Red"; c: number }
  | { t: "Green"; c: number }
  | { t: "Blue"; c: number };

export interface Person {
  name: string;
  age: number;
  enjoys_coffee: boolean;
}

export interface ComplexType {
  colour_map: HashMap<string, Colour>;
  list_of_names: Vec<string>;
  optional_person: Option<Person>;
}
```

So now that we have the ability to share types between Rust and Typescript!

# Tests

It's always a good idea to add tests to your program/crate!  Let's add some basic ones to test that we get the output for our sample types that we have been working with.

I'll start by creating a `tests` in `src`.  I'm going to create three files total:

```
src/tests/type.rs
src/tests/enum.rs
src/tests/struct.rs
```

These files will just have the three basic sample types we have been working with:

`src/tests/type.rs`
```rust
type NumberAlias = i32;
```

`src/tests/enum.rs`
```rust
#[serde(tag = "t", content = "c")]
enum Colour {
    Red(i32),
    Green(i32),
    Blue(i32),
}
```

`src/tests/struct.rs`
```rust
struct Person {
    name: String,
    age: u32,
    enjoys_coffee: bool,
}
```

Next I'll write some simple test that read in these files and assert that the output matches the Typescript output that we expect, so that we can safely expand our type generating utility in the future and be able to know immediately by running our tests that we haven't broken any existing functionality:

For now I'm simply going to add this testing module down at the bottom of `main.rs` below the program itself and I'll use `use super::*` so I have access to all the same modules that the main program does:



`main.rs`
```rust

#[cfg(test)]
mod tests {

    use super::*;

    #[test]
    fn handles_type_alias() {
        let mut input_file = File::open("./src/tests/type.rs").unwrap();

        let mut input_file_text = String::new();

        input_file.read_to_string(&mut input_file_text).unwrap();

        let input_syntax: syn::File =
            syn::parse_file(&input_file_text).expect("Unable to parse file");

        let typescript_types = parse_syn_file(input_syntax);

        assert_eq!(r#"export type NumberAlias = number;"#, &typescript_types);
    }

    #[test]
    fn handles_struct() {
        let mut input_file = File::open("./src/tests/struct.rs").unwrap();

        let mut input_file_text = String::new();

        input_file.read_to_string(&mut input_file_text).unwrap();

        let input_syntax: syn::File =
            syn::parse_file(&input_file_text).expect("Unable to parse file");

        let typescript_types = parse_syn_file(input_syntax);

        assert_eq!(
            r#"export interface Person {name:string;age:number;enjoys_coffee:boolean;};"#,
            &typescript_types
        );
    }

    #[test]
    fn handles_enum() {
        let mut input_file = File::open("./src/tests/enum.rs").unwrap();

        let mut input_file_text = String::new();

        input_file.read_to_string(&mut input_file_text).unwrap();

        let input_syntax: syn::File =
            syn::parse_file(&input_file_text).expect("Unable to parse file");

        let typescript_types = parse_syn_file(input_syntax);

        assert_eq!(
            r#"export type Colour =  | { t: "Red" , c: number} | { t: "Green" , c: number} | { t: "Blue" , c: number};"#,
            &typescript_types
        );
    }
}
```


When we run the following command on the terminal:

```
cargo test
```

We would see the output:

```
running 3 tests
test tests::handles_type_alias ... ok
test tests::handles_enum ... ok
test tests::handles_struct ... ok
```

Remember to keep your tests updated as you expand your utility!


# Building and Publishing

There are a few different ways we can use and share our program that we'll go over quickly.

The first is with the command we have been using to do build and run a development version:

```
cargo run -- --input=src/types.rs --output=types.d.ts
```

We can also build a release version with all the optimizations using:

```
cargo run --release
```

Then you'll find the `typester` binary in the `target/release` folder.  You can now place it anywhere on your machine and run it from the command line by navigating to the directory and invoking:

```
./typester --input=src/types.rs --output=types.d.ts
```

The last method if you want to share your utility with others, or use it on other machines is to publish it to [crates.io](https://crates.io/).  

You'll first need to upload your project to Github and make a crates.io account if you don't have one already.  [This tutorial will take you through all the steps you need to publish your crate.](https://doc.rust-lang.org/cargo/reference/publishing.html)

Now you can install your CLI locally with `cargo install YOUR_CRATE_NAME`.  

You can see here I've [published this utility to crates.io](https://crates.io/crates/typester) and now I can install it on any machine that has cargo installed with:

```
cargo install typester
```

Which installs by default in your `$HOME/.cargo` directory.  Now you can use the utility on any directory on your machine!

# Future Improvements

There's plenty of options for improving this crate you can pursue beyond just improving support for additional types within Rust's ecosystem.  

Here's a few examples:

- Target an entire project directory rather than just a single file with the [walkdir](https://crates.io/crates/walkdir) crate

- Capture [doc comments](https://doc.rust-lang.org/rust-by-example/meta/doc.html) for your types and convert them to [jsdoc comments](https://jsdoc.app/about-getting-started.html) so your frontend dev team knows exactly how to use your types

- Custom attributes.  We've already talked about parsing serde's attributes, what what about custom ones specifically for your crate like the ability to `#[ignore]` types that aren't intended for frontend consumption?

- Camel case.  Have your CLI watch for serde's [rename attribute](https://serde.rs/container-attrs.html) and transform your struct fields into Typescript's standard of `camelCase` from Rust's `snake_case`

- Generics!  Can you include support for them.  I don't even know where to start with that one, but it would probably be a great challenge.  

I'm sure you can come up with other great ideas too, feel free to comment on this post with your own!

# Similar Open Source Tools

If you are actually looking for a more robust version of this kind of tool and not interest in writing it yourself, there are a number of very similar and more feature rich versions of this utility out there already in the wild:

- [Typeshare](https://github.com/1Password/typeshare) by [1Password](https://1password.com/), the original inspiration for this blog post.  It's available on [crates.io](https://crates.io/crates/typeshare).  

- [tsync](https://github.com/Wulf/create-rust-app) by Wulf created as part of the awesome [Create Rust App](https://github.com/Wulf/create-rust-app) utility for setting up fullstack Rust apps.  It's available on [crates.io](https://crates.io/crates/tsync)

# Conclusion

I hope you learned something new about how Rust itself can be parsed with crates like `syn` and different kinds of helpful utilities can be built out of it.

There's no reason you can't add support for other languages you use too!  I simply chose Typescript as it's the one I'm most familiar with.

In a [follow up blog post](https://dev.to/alexeagleson/how-to-set-up-a-fullstack-rust-project-with-axum-react-vite-and-shared-types-429e), we'll take a look at how to actually use this utility in a real fullstack application with an
[Axum](https://github.com/tokio-rs/axum) backend in Rust, and React with [Vite](https://vitejs.dev/) for the frontend.