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.
- Tapper.Attributes
- Tapper.Analyzer
- Tapper.Generator (.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, but your app TFM can use .NET 6, etc.
dotnet tool install --global Tapper.Generator
tapper help
# update
dotnet tool update --global Tapper.Generator
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);
}
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.
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 |
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> | { [key: TKey]: TValue } | |
IDictionary<TKey, TValue> | { [key: TKey]: TValue } | |
IReadOnlyDictionary<TKey, TValue> | { [key: TKey]: TValue } | |
Tuple | [T1, T2, ...] | ❌ System.Text.Json |
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.
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[];
}
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
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";
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.
-
Json
- C# : System.Text.Json
- TypeScript : JSON.stringify()
-
MessagePack
- C# : MessagePack-CSharp
- TypeScript : msgpack-javascript
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; }
}
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]
// 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;
}
// 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;
}
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
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.
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