Skip to content

A Tool Transpiling C# Type into TypeScript Type. (Support JSON & MessagePack Serialization)

License

Notifications You must be signed in to change notification settings

nenoNaninu/Tapper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tapper

NuGet build-and-test

Tapper is a library/CLI tool to transpile C# type (class, struct, record, enum) into TypeScript type (type, enum). Using this tool can reduce serialization bugs (type mismatch, typos, etc.) and make TypeScript code easily follow changes in C# code.

teaser

Table of Contents

Packages

Install Using .NET Tool

Use Tapper.Generator(CLI Tool) to generate TypeScript type from C# type. Tapper.Generator can be easily installed using .NET Global Tools. You can use the installed tools with the command tapper.

# install
# Tapper CLI (dotnet tool) requires .NET 7 or .NET 8, but your app TFM can use .NET 6, etc.
dotnet tool install --global Tapper.Generator
tapper help

# update
dotnet tool update --global Tapper.Generator

Getting Started

First, add the following packages to your project. Tapper.Analyzer is optional, but recommended.

dotnet add package Tapper.Attributes
dotnet add package Tapper.Analyzer (optional, but recommended.)

Next, apply the [TranspilationSource] Attribute to the C# type definition.

using Tapper;

namespace SampleNamespace;

[TranspilationSource] // <- Add attribute!
public class SampleType
{
    public List<int>? List { get; }
    public int Value { get; }
    public Guid Id { get; }
    public DateTime DateTime { get; }
}

Then execute the command as follows.

tapper --project path/to/XXX.csproj --output outdir

TypeScript source code is generated in the directory specified by --output. In this example, TypeScript source code named outdir/SampleNamespace.ts is generated. The contents of the generated code is as follows.

/* eslint-disable */

/** Transpiled from SampleNamespace.SampleType */
export type SampleType = {
  /** Transpiled from System.Collections.Generic.List<int>? */
  list?: number[];
  /** Transpiled from int */
  value: number;
  /** Transpiled from System.Guid */
  id: string;
  /** Transpiled from System.DateTime */
  dateTime: (Date | string);
}

Transpilation Rules

Tapper transpile C# types (class, struct, record, enum) to TypeScript types (type, enum). When transpiling class, struct, and record, only public fields and properties are transpiled.

Built-in Supported Types

C# TypeScript Description
bool boolean
byte number
sbyte number
char string or number JSON: string, MessagePack number.
decimal number
double number
float number
int number
uint number
long number
ulong number
short number
ushort number
object any
string string
Uri string
Guid string Compatible with TypeScript's crypto.randomUUID().
DateTime (Date | string) or Date Json: (Date | string), MessagePack: Date.
DateTimeOffset (Date | string) or [Date, number] Json: (Date | string), MessagePack: [Date, number]. note #41
TimeSpan string or number Json: string, MessagePack: number.
System.Nullable<T> (T | undefined)
byte[] string or Uint8Array JSON: string (base64), MessagePack Uint8Array.
T[] T[]
System.Array any[] ❌ System.Text.Json
ArraySegment<T> T[] ❌ System.Text.Json
List<T> T[]
LinkedList<T> T[]
Queue<T> T[]
Stack<T> T[]
HashSet<T> T[]
IEnumerable<T> T[]
IReadOnlyCollection<T> T[]
ICollection<T> T[]
IList<T> T[]
ISet<T> T[]
Dictionary<TKey, TValue> Partial<Record<TKey, TValue>>
IDictionary<TKey, TValue> Partial<Record<TKey, TValue>>
IReadOnlyDictionary<TKey, TValue> Partial<Record<TKey, TValue>>
Tuple [T1, T2, ...] ❌ System.Text.Json

C# Namespace

C# namespace is mapped to the filename of the generated TypeScript code.

namespace SampleNamespace;

[TranspilationSource]
record Xxx();

For example, given the above C# code, TypeScript code with the file name SampleNamespace.ts is generated.

Nesting Types

It doesn't matter if the user-defined types are nested. For example, consider the following C# code. Apply [TranspilationSource] Attribute to all types to be transpiled. If you add an analyzer package, you can avoid forgetting to apply [TranspilationSource].

#nullable enable
using System.Text.Json.Serialization;
using Tapper;

namespace Space1
{
    [TranspilationSource]
    public class CustomType1
    {
        public int Value;
        public Guid Id;
        [JsonIgnore]
        public string Foo;
    }

    namespace Sub
    {
        [TranspilationSource]
        public enum MyEnum
        {
            Zero = 0,
            One = 1,
            Two = 1 << 1,
            Four = 1 << 2,
        }
    }
}

namespace Space2
{
    [TranspilationSource]
    public record CustomType3(float Value, DateTime ReleaseDate);
}

namespace Space3
{
    using Space1;
    using Space1.Sub;
    using Space2;

    [TranspilationSource]
    public class NastingNamespaceType
    {
        public CustomType1? Value { get; set; }
        public MyEnum MyEnumValue { get; set; }
        [JsonPropertyName("list")]
        public List<CustomType3> MyList { get; set; } = new();
    }
}

The following TypeScript code is generated.

  • Space1.ts
/** Transpiled from Space1.CustomType1 */
export type CustomType1 = {
  /** Transpiled from int */
  value: number;
  /** Transpiled from System.Guid */
  id: string;
}
  • Space1.Sub.ts
/** Transpiled from Space1.Sub.MyEnum */
export enum MyEnum {
  Zero = 0,
  One = 1,
  Two = 2,
  Four = 4,
}
  • Space2.ts
/** Transpiled from Space2.CustomType3 */
export type CustomType3 = {
  /** Transpiled from float */
  value: number;
  /** Transpiled from System.DateTime */
  name: (Date | string);
}
  • Space3.ts
import { CustomType1 } from './Space1';
import { MyEnum } from './Space1.Sub';
import { CustomType3 } from './Space2';

/** Transpiled from Space3.NastingNamespaceType */
export type NastingNamespaceType = {
  /** Transpiled from Space1.CustomType1? */
  value?: CustomType1;
  /** Transpiled from Space1.Sub.MyEnum */
  myEnumValue: MyEnum;
  /** Transpiled from System.Collections.Generic.List<Space2.CustomType3> */
  list: CustomType3[];
}

Options

Naming Style

You can select camelCase, PascalCase, or none for the property name of the generated TypeScript type. For none, the property name in C# is used. The default is the standard naming style for TypeScript.

tapper --project path/to/Xxx.csproj --output outdir --naming-style camelCase

Enum Style

There are options for enum transpiling. You can select Value (default), Name, NameCamel, NamePascal, Union, UnionCamel, or UnionPascal. If you use this option, be careful with the serializer options. For example, System.Text.Json serializes an enum as a integer by default (not string). To serialize an enum as a string, you must pass JsonStringEnumConverter as an option to JsonSerializer.

tapper --project path/to/Xxx.csproj --output outdir --enum value
tapper --project path/to/Xxx.csproj --output outdir --enum name
tapper --project path/to/Xxx.csproj --output outdir --enum union
// C# source
[TranspilationSource]
public enum MyEnum
{
    Zero = 0,
    One = 1,
    Two = 1 << 1,
    Four = 1 << 2,
}
// Generated TypeScript

// --enum value (default)
export enum MyEnum {
  Zero = 0,
  One = 1,
  Two = 2,
  Four = 4,
}

// --enum name
export enum MyEnum {
  Zero = "Zero",
  One = "One",
  Two = "Two",
  Four = "Four",
}

// --enum union
export type MyEnum = "Zero" | "One" | "Two" | "Four";

// --enum unionCamel
export type MyEnum = "zero" | "one" | "two" | "four";

Serializer

The TypeScript code generated by Tapper is supposed to be serialized/deserialized with json or MessagePack. And the appropriate type is slightly different depending on the serializer. You can specify which one to use by passing the --serializer option. The default is json.

tapper --project path/to/Xxx.csproj --output outdir --serializer MessagePack --naming-style none

Also, it is supposed that the following serializers are used.

MessagePack

If you use MessagePack-CSharp for the serializer, be careful how you apply the [MessagePackObject] Attribute. It is recommended to use [MessagePackObject(true)]. Also, in that case, set --naming-style to none.

tapper --project path/to/Xxx.csproj --output outdir --serializer MessagePack --naming-style none
[MessagePackObject(true)] // <- use this!
public class SampleType
{
    public Guid Id { get; set; }
    public int Value { get; set; }
}

Serializer Attributes Support

Tapper reflects JSON and MessagePack serializer attributes in the output TypeScript code.

Support attributes:

  • System.Text.Json.Serialization
    • [JsonPropertyName("string")]
    • [JsonIgnore]
  • MessagePack
    • [Key("string")]
    • [IgnoreMember]

JSON

// input C# code
// --serializer json

namespace Readme;

[TranspilationSource]
public class Type1
{
    [JsonIgnore]
    public required int Value { get; init; }
    public required string Name { get; init; }
}

[TranspilationSource]
public class Type2
{
    [JsonPropertyName("Foo")]
    public required int Value { get; init; }
    public required string Name { get; init; }
}
// output TypeScript code

/** Transpiled from Readme.Type1 */
export type Type1 = {
    /** Transpiled from string */
    name: string;
}

/** Transpiled from Readme.Type2 */
export type Type2 = {
    /** Transpiled from int */
    Foo: number;
    /** Transpiled from string */
    name: string;
}

MessagePack

// input C# code
// --serializer MessagePack --naming-style none

namespace Readme;

[TranspilationSource]
public class Type3
{
    [IgnoreMember]
    public required Value { get; init; }
    public required string Name { get; init; }
}

[TranspilationSource]
public class Type4
{
    [Key("Bar")]
    public required int Value { get; init; }
    public required string Name { get; init; }
}
// output TypeScript code

/** Transpiled from Readme.Type3 */
export type Type3 = {
    /** Transpiled from string */
    Name: string;
}

/** Transpiled from Readme.Type4 */
export type Type4 = {
    /** Transpiled from int */
    Bar: number;
    /** Transpiled from string */
    Name: string;
}

Transpile the Types Contained in Referenced Assemblies

By default, only types defined in the project specified by the --project option are targeted for transpiling. By passing the -asm true option, types contained in project/package reference assemblies will also be targeted for transpiling.

tapper --project path/to/Xxx.csproj --output outdir -asm true

Analyzer

Tapper has some rules. You can easily follow those rules by adding Tapper.Analyzer.

  • If the fields and property types contained in the type to which [TranspilationSource] applies are user-defined types, you must also apply [TranspilationSource] to those types.
  • You cannot apply [TranspilationSource] to Generic type.

Unity Support

For Unity projects, first, copy and paste the TranspilationSourceAttribute.cs into your project. Then apply [TranspilationSource] to types you want to transpile.

Next, a file named Assembly-CSharp.csproj is generated by Unity. It is in the same hierarchy as Assets. Use this project file as an argument to --project.

tapper --project path/to/Assembly-CSharp.csproj --output outdir

Related Work