Types in N#
This guide covers the type system in N#, including classes, structs, records, discriminated unions, duck interfaces, and enums.
Table of Contents
- Basic Types
- Classes
- Structs
- Records
- Discriminated Unions
- Duck Interfaces
- Enums
- Interfaces
- Generics
- Nullable Types
Basic Types
N# uses .NET's type system:
// Primitives
x: int = 42
y: long = 1000000
z: double = 3.14
w: decimal = 19.99
flag: bool = true
letter: char = 'A'
// String
name: string = "Alice"
message: string? = null // Nullable string
// Arrays
numbers: int[] = [1, 2, 3, 4, 5]
names: string[] = ["Alice", "Bob", "Charlie"]
Classes
Basic Class Declaration
class Person {
FirstName: string
LastName: string
Age: int
constructor(firstName: string, lastName: string, age: int) {
FirstName = firstName
LastName = lastName
Age = age
}
func getFullName(): string {
return $"{FirstName} {LastName}"
}
}
Primary Constructors (C# 12)
class Person(firstName: string, lastName: string, age: int) {
FirstName: string = firstName
LastName: string = lastName
Age: int = age
func getFullName(): string => $"{FirstName} {LastName}"
}
Properties
class Product {
// Auto-property
Name: string
// Auto-property with initializer
Price: decimal = 0.0
// Expression-bodied property
DisplayName: string => $"{Name} (${Price})"
// Full property with getter and setter
stock: int
Stock: int {
get => stock
set {
if value < 0 {
throw new ArgumentException("Stock cannot be negative")
}
stock = value
}
}
}
Init-only Properties
class Configuration {
AppName: string { get; init; }
Version: string { get; init; }
}
// Usage
config := new Configuration {
AppName: "MyApp",
Version: "1.0"
}
// config.AppName = "NewName" // Error: init-only property
Required Properties
class User {
required Id: Guid
required Name: string
Email: string? // Optional
}
// Must initialize required properties
user := new User {
Id: Guid.NewGuid(),
Name: "Alice"
}
Inheritance
class Animal {
Name: string
constructor(name: string) {
Name = name
}
virtual func makeSound(): string {
return "..."
}
}
class Dog : Animal {
constructor(name: string) : base(name) {
}
override func makeSound(): string {
return "Woof!"
}
}
Abstract Classes
abstract class Shape {
abstract func getArea(): double
abstract func getPerimeter(): double
func describe() {
Console.WriteLine($"Area: {getArea()}, Perimeter: {getPerimeter()}")
}
}
class Circle : Shape {
Radius: double
constructor(radius: double) {
Radius = radius
}
override func getArea(): double {
return Math.PI * Radius * Radius
}
override func getPerimeter(): double {
return 2 * Math.PI * Radius
}
}
Static Members
class MathHelper {
static Pi: double = 3.14159
static func square(x: double): double {
return x * x
}
}
// Usage
result := MathHelper.square(5)
pi := MathHelper.Pi
Structs
Structs are value types:
struct Point {
X: double
Y: double
constructor(x: double, y: double) {
X = x
Y = y
}
func distanceFrom(other: Point): double {
dx := X - other.X
dy := Y - other.Y
return Math.Sqrt(dx * dx + dy * dy)
}
}
// Usage
p1 := new Point(0, 0)
p2 := new Point(3, 4)
distance := p1.distanceFrom(p2) // 5.0
Readonly Structs
readonly struct Vector3 {
X: double
Y: double
Z: double
constructor(x: double, y: double, z: double) {
X = x
Y = y
Z = z
}
}
Records
Records are immutable reference types:
record Person {
FirstName: string
LastName: string
Age: int
}
// Usage
person := new Person {
FirstName: "Alice",
LastName: "Smith",
Age: 30
}
// With expressions (create modified copy)
older := person with { Age: 31 }
Record Structs
record struct Point {
X: double
Y: double
}
// Value semantics with record features
p1 := new Point { X: 1, Y: 2 }
p2 := p1 with { X: 3 }
Positional Records
record Person(string FirstName, string LastName, int Age)
// Usage
person := new Person("Alice", "Smith", 30)
Console.WriteLine(person.FirstName) // "Alice"
// Deconstruction
(first, last, age) := person
Discriminated Unions
Unions are N#'s most powerful feature - they provide type-safe alternatives:
Basic Union
union Result<T> {
Success { value: T }
Failure { error: string }
}
// Usage
func divide(a: double, b: double): Result<double> {
if b == 0 {
return new Result.Failure<double> { error: "Division by zero" }
}
return new Result.Success<double> { value: a / b }
}
Pattern Matching with Unions
result := divide(10, 2)
message := match result {
Result.Success<double> { value: v } => $"Result: {v}",
Result.Failure<double> { error: e } => $"Error: {e}"
}
Union with Multiple Fields
union HttpResponse {
Ok { body: string, statusCode: int }
Error { message: string, code: int }
Redirect { url: string, permanent: bool }
}
func handleResponse(response: HttpResponse) {
match response {
HttpResponse.Ok { body, statusCode } => {
Console.WriteLine($"Success ({statusCode}): {body}")
},
HttpResponse.Error { message, code } => {
Console.WriteLine($"Error {code}: {message}")
},
HttpResponse.Redirect { url, permanent } => {
redirectType := if permanent { "permanent" } else { "temporary" }
Console.WriteLine($"Redirect ({redirectType}): {url}")
}
}
}
Option Type
union Option<T> {
Some { value: T }
None { }
}
func findUser(id: int): Option<User> {
user := database.Find(id)
if user == null {
return new Option.None<User> { }
}
return new Option.Some<User> { value: user }
}
How Unions Compile to C#
N# unions compile to C# class hierarchies:
union Result<T> {
Success { value: T }
Failure { error: string }
}
Compiles to:
public abstract class Result<T>
{
private Result() { }
public sealed class Success : Result<T>
{
public T Value { get; init; }
}
public sealed class Failure : Result<T>
{
public string Error { get; init; }
}
}
This means C# code can use N# unions naturally:
// C# code consuming N# union
var result = new Result<int>.Success { Value = 42 };
Duck Interfaces
Duck interfaces provide structural typing - types match based on their shape:
duck interface IReader {
func Read(): string
}
class FileReader {
func Read(): string {
return "file contents"
}
}
class HttpReader {
func Read(): string {
return "http contents"
}
}
func processReader(reader: IReader) {
content := reader.Read()
Console.WriteLine(content)
}
// Both types work - they have the right shape!
processReader(new FileReader())
processReader(new HttpReader())
Duck Interface Constraints
duck interface IProcessor<T> {
func Process(input: T): T
}
class StringProcessor {
func Process(input: string): string {
return input.ToUpper()
}
}
func execute<T>(processor: IProcessor<T>, value: T): T {
return processor.Process(value)
}
// Usage
result := execute<string>(new StringProcessor(), "hello")
How Duck Interfaces Compile
Duck interfaces are compile-time only - they're erased at runtime:
duck interface IReader {
func Read(): string
}
Compiles to an internal interface:
internal interface IReader
{
string Read();
}
And the compiler automatically implements it on matching types:
// Original N#
class FileReader {
func Read(): string { ... }
}
// Generated C#
class FileReader : IReader
{
public string Read() { ... }
}
Enums
N# supports string enums for better APIs:
enum Status {
Active = "active",
Inactive = "inactive",
Pending = "pending"
}
enum Role {
Admin = "admin",
User = "user",
Guest = "guest"
}
Using Enums
userStatus: string = Status.Active
userRole: string = Role.Admin
// In functions
func checkAccess(role: string): bool {
return role == Role.Admin
}
How Enums Compile
String enums compile to static classes:
public static class Status
{
public const string Active = "active";
public const string Inactive = "inactive";
public const string Pending = "pending";
}
Interfaces
Basic Interfaces
interface ICalculator {
func Add(a: int, b: int): int
func Subtract(a: int, b: int): int
}
class BasicCalculator : ICalculator {
func Add(a: int, b: int): int => a + b
func Subtract(a: int, b: int): int => a - b
}
Interface Properties
interface IEntity {
Id: Guid { get; }
Name: string { get; set; }
}
class User : IEntity {
Id: Guid { get; }
Name: string { get; set; }
constructor() {
Id = Guid.NewGuid()
}
}
Generic Interfaces
interface IRepository<T> {
func GetById(id: Guid): T?
func GetAll(): List<T>
func Add(entity: T): void
func Delete(id: Guid): bool
}
class UserRepository : IRepository<User> {
users: List<User> = new List<User>()
func GetById(id: Guid): User? {
return users.FirstOrDefault(u => u.Id == id)
}
func GetAll(): List<User> => users
func Add(entity: User) {
users.Add(entity)
}
func Delete(id: Guid): bool {
user := GetById(id)
if user != null {
users.Remove(user)
return true
}
return false
}
}
Generics
Generic Classes
class Container<T> {
value: T
constructor(value: T) {
this.value = value
}
func GetValue(): T => value
func SetValue(newValue: T) {
value = newValue
}
}
// Usage
intContainer := new Container<int>(42)
stringContainer := new Container<string>("hello")
Generic Constraints
// Class constraint
class Repository<T> where T : class {
items: List<T> = new List<T>()
func Add(item: T) {
items.Add(item)
}
}
// Struct constraint
class ValueContainer<T> where T : struct {
value: T?
func HasValue(): bool => value != null
}
// Interface constraint
class Processor<T> where T : IComparable<T> {
func GetMax(a: T, b: T): T {
return if a.CompareTo(b) > 0 { a } else { b }
}
}
// Constructor constraint
class Factory<T> where T : new() {
func Create(): T {
return new T()
}
}
Multiple Constraints
class Service<T> where T : class, IDisposable, new() {
func CreateAndUse() {
instance := new T()
try {
// Use instance
} finally {
instance.Dispose()
}
}
}
Nullable Types
Nullable Reference Types
// Non-nullable (default)
name: string = "Alice"
// name = null // Error!
// Nullable
optionalName: string? = null
optionalName = "Bob" // OK
Nullable Value Types
age: int? = null
age = 25
if age != null {
Console.WriteLine($"Age: {age.Value}")
}
// Null-coalescing operator
displayAge := age ?? 0
Null-conditional Operator
user: User? = GetUser()
name := user?.Name // null if user is null
// Chaining
city := user?.Address?.City
Null-forgiving Operator
// When you know it's not null
name: string = optionalName!
Type Aliases
// Not yet supported in N# - use C# using directives
// Future feature:
// type UserId = Guid
// type EmailAddress = string
Complete Example
Here's a complete example demonstrating various type features:
import System
import System.Linq
import System.Collections.Generic
package TypesExample
// Enum
enum Status {
Active = "active",
Inactive = "inactive"
}
// Duck interface
duck interface IIdentifiable {
Id: Guid { get; }
}
// Record
record Address {
Street: string
City: string
State: string
ZipCode: string
}
// Class with primary constructor
class Person(id: Guid, name: string, age: int) : IIdentifiable {
Id: Guid = id
Name: string = name
Age: int = age
Address: Address?
func describe(): string =>
$"{Name}, {Age} years old (ID: {Id})"
}
// Union
union Result<T> {
Success { value: T }
Failure { error: string }
}
// Generic class with constraints
class Repository<T> where T : IIdentifiable {
items: List<T> = new List<T>()
func Add(item: T) {
items.Add(item)
}
func GetById(id: Guid): Result<T> {
found := items.FirstOrDefault(i => i.Id == id)
return match found {
null => new Result.Failure<T> { error: "Not found" },
_ => new Result.Success<T> { value: found }
}
}
}
func main() {
// Create a repository
repo := new Repository<Person>()
// Create a person
person := new Person(Guid.NewGuid(), "Alice", 30) {
Address: new Address {
Street: "123 Main St",
City: "New York",
State: "NY",
ZipCode: "10001"
}
}
// Add to repository
repo.Add(person)
// Retrieve and match
result := repo.GetById(person.Id)
match result {
Result.Success<Person> { value: p } => {
Console.WriteLine($"Found: {p.describe()}")
if p.Address != null {
Console.WriteLine($"Lives in: {p.Address.City}")
}
},
Result.Failure<Person> { error: e } => {
Console.WriteLine($"Error: {e}")
}
}
}
Next Steps
- Pattern Matching - Deep dive into pattern matching with unions and more
- Functions Guide - Learn about functions, lambdas, and async
- Interop Guide - Using N# with C# and .NET libraries