newtype 0.6.3
dotnet add package newtype --version 0.6.3
NuGet\Install-Package newtype -Version 0.6.3
<PackageReference Include="newtype" Version="0.6.3"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="newtype" Version="0.6.3" />
<PackageReference Include="newtype"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add newtype --version 0.6.3
#r "nuget: newtype, 0.6.3"
#:package newtype@0.6.3
#addin nuget:?package=newtype&version=0.6.3
#tool nuget:?package=newtype&version=0.6.3
newtype (Distinct Type Aliases for C#)
A source generator that creates distinct type aliases with full operator, constructor, and member forwarding. Inspired by Haskell's newtype. Works with primitives, structs, records, and classes.
dotnet add package newtype
Quick Start
using newtype;
// Typed IDs — no more mixing up int parameters
[newtype<int>]
public readonly partial struct PlayerId;
// Domain quantities — Position and Velocity can't be accidentally swapped
[newtype<Vector3>]
public readonly partial struct Position;
[newtype<Vector3>]
public readonly partial struct Velocity;
// Rich string aliases — all string operations forwarded
[newtype<string>]
public readonly partial struct Email;
// Also works with records, classes, and record classes
[newtype<double>]
public partial record struct Duration; // gets compiler-synthesized equality
[newtype<string>]
public partial class DisplayName; // reference-type wrapper with null safety
Everything works as you'd expect:
var id = new PlayerId(42);
PlayerId next = id + 1; // arithmetic forwarded
var pos = new Position(1, 2, 3); // constructor forwarded from Vector3
Console.WriteLine(pos.X); // instance members forwarded
Position origin = Position.Zero; // static members forwarded
Email addr = "alice@example.com"; // implicit conversion from string
string raw = addr; // implicit conversion to string
Email greeting = addr + " (verified)"; // string concatenation works
void Move(Position p, Velocity v) { ... }
Move(velocity, position); // compile error — type safety!
What Gets Generated
For each [newtype<T>] partial type, the generator emits:
| Category | What |
|---|---|
| Core | Backing field, Value property, constructor from T, forwarded constructors |
| Conversions | Implicit operators both ways (T ↔ alias) |
| Operators | Binary (+ - * / % & \| ^ << >>), unary (- + ! ~ ++ --), comparison, equality |
| Static members | Properties and readonly fields (e.g. Vector3.UnitX → Position.UnitX) |
| Instance members | Properties and methods (e.g. .X, .Length()) |
| Interfaces | IEquatable<T>, IComparable<T>, IFormattable (when the aliased type implements them) |
All generated members use [MethodImpl(MethodImplOptions.AggressiveInlining)] by default for zero-cost abstraction.
Constraining Generation
Use NewtypeOptions to suppress features, and MethodImpl to control inlining:
// No implicit int -> PlayerId (must use constructor)
[newtype<int>(Options = NewtypeOptions.NoImplicitWrap)]
public readonly partial struct PlayerId;
// No implicit conversions either way
[newtype<int>(Options = NewtypeOptions.NoImplicitConversions)]
public readonly partial struct OpaqueId;
// Fully locked down — no conversions, no forwarded constructors
[newtype<Vector3>(Options = NewtypeOptions.Opaque)]
public readonly partial struct StrictPosition;
// Let the JIT decide about inlining (omit [MethodImpl] entirely)
[newtype<int>(MethodImpl = default)]
public readonly partial struct RelaxedId;
NewtypeOptions Flags
| Flag | Effect |
|---|---|
None |
All features enabled (default) |
NoImplicitWrap |
Suppress T → alias implicit conversion |
NoImplicitUnwrap |
Suppress alias → T implicit conversion |
NoConstructorForwarding |
Suppress forwarded constructors from T |
NoImplicitConversions |
NoImplicitWrap \| NoImplicitUnwrap |
Opaque |
NoImplicitConversions \| NoConstructorForwarding |
Serializable |
Generate System.Text.Json + TypeConverter serialization support |
With any option, the primary constructor (new Alias(T value)) and .Value property are always available.
Serialization
By default a newtype serializes like any struct — which usually isn't what you want. Opt in with NewtypeOptions.Serializable to make it serialize as its underlying value:
[newtype<string>(Options = NewtypeOptions.Serializable)]
public readonly partial struct ContractId;
JsonSerializer.Serialize(new ContractId("CTR-002")); // "CTR-002"
This generates a [JsonConverter] (for System.Text.Json) and a [TypeConverter] (for Newtonsoft.Json, ASP.NET model binding, configuration binding, dictionary keys, etc.). Both delegate the underlying value to T's own serializer, so any serializable T works.
XmlSerializerround-tripping is not supported forreadonly structnewtypes — it populates instances in place, which an immutable struct forbids.
Extending Your Types
Since the alias is a partial type, you can add custom members:
[newtype<Vector3>]
public readonly partial struct Position
{
public static Position operator +(Position p, Velocity v)
=> new(p.Value + v.Value);
}
Viewing Generated Code
Enable generated file output in your project:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
Then inspect obj/Debug/net10.0/GeneratedFiles/ after building.
Naming
The attribute is intentionally lowercase newtype<T>: it mirrors the newtype namespace, the NuGet package id, and Haskell's newtype keyword. The class itself keeps the conventional Attribute suffix internally (newtypeAttribute<T>), so both [newtype<T>] and the [newtype(typeof(T))] fallback bind correctly.
Requirements
- .NET 8+ SDK
- C# 11+ (for generic attributes
[newtype<T>]; a[newtype(typeof(T))]fallback exists for older versions)
Known Limitations
Indexers, generic instance methods, ref returns, events, and nested types are not forwarded. PRs welcome!
License
MIT
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.