TS
C#

TypeScript to C#

10 lessons

Progress0%
1Introduction: Two Languages, One Mind2Type Systems: Structural vs Nominal3Classes: Advanced Features4Generics and LINQ5Nullable Reference Types6Async/Await7Decorators to Attributes8Ecosystem9File I/O10Records and Pattern Matching
All Mirror Courses
TS
C#
Records and Pattern Matching
MirrorLesson 10 of 10
Lesson 10

Records and Pattern Matching

C# record types provide value-based equality and immutability; switch expressions with property patterns replace TypeScript discriminated unions and complex conditionals.

Introduction

In this lesson, you'll learn about records and pattern matching in C#. Coming from TypeScript, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.

Mirror Card
TS
From TypeScript:

In TypeScript, you're familiar with c# record types provide value-based equality and immutability; switch expressions with property patterns replace typescript discriminated unions and complex conditionals..

C#
In C#:

C# has its own approach to c# record types provide value-based equality and immutability; switch expressions with property patterns replace typescript discriminated unions and complex conditionals., which we'll explore step by step.

The C# Way

Let's see how C# handles this concept. Here's a typical example:

C#
C# Example
// record: immutable, value-based equality, with-expression
record Point(double X, double Y);
record Circle(double Radius) : Shape;
record Rect(double Width, double Height) : Shape;
record Triangle(double Base, double Height) : Shape;

abstract record Shape;

// Switch expression with property patterns
static double Area(Shape s) => s switch {
    Circle  { Radius: var r }            => Math.PI * r * r,
    Rect    { Width: var w, Height: var h } => w * h,
    Triangle { Base: var b, Height: var h } => 0.5 * b * h,
    _ => throw new ArgumentException("Unknown shape")
};

// Value equality — records compare by properties
var p1 = new Point(1, 2);
var p2 = new Point(1, 2);
Console.WriteLine(p1 == p2);    // True (value equality!)
Console.WriteLine(p1.Equals(p2)); // True

// with-expression: non-destructive mutation
var p3 = p1 with { X = 10 };    // new Point(10, 2)
Console.WriteLine(p3);          // Point { X = 10, Y = 2 }

Comparing to TypeScript

Here's how you might have written similar code in TypeScript:

TS
TypeScript (What you know)
// TypeScript discriminated union
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rect"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle":   return Math.PI * s.radius ** 2;
    case "rect":     return s.width * s.height;
    case "triangle": return 0.5 * s.base * s.height;
  }
}

// Immutable data objects
type Point = { readonly x: number; readonly y: number };
const p: Point = { x: 1, y: 2 };
const p2 = { ...p, x: 10 };   // "update" via spread

// Equality is reference-based in JS
console.log({ x: 1 } === { x: 1 }); // false
Mirror Card
TS
From TypeScript:

You may be used to different syntax or behavior.

C#
In C#:

record types have value-based equality by default (== compares properties) — unlike classes where == is reference equality

Mirror Card
TS
From TypeScript:

You may be used to different syntax or behavior.

C#
In C#:

with expression creates a copy with specified properties changed — equivalent to object spread in TypeScript

Mirror Card
TS
From TypeScript:

You may be used to different syntax or behavior.

C#
In C#:

switch expression (not statement) with => returns a value; exhaustiveness checked by compiler

Mirror Card
TS
From TypeScript:

You may be used to different syntax or behavior.

C#
In C#:

Property patterns ({ Radius: var r }) extract and bind values in one step

Mirror Card
TS
From TypeScript:

You may be used to different syntax or behavior.

C#
In C#:

TypeScript discriminated unions use a 'kind' tag; C# pattern matching works on the actual type

Step-by-Step Breakdown

1. Record Types

Records are classes with automatically generated constructor, value equality, and ToString. Use for immutable data transfer objects.

TS
TypeScript
type Point = { readonly x: number; readonly y: number };
const p = { x: 1, y: 2 };
C#
C#
record Point(double X, double Y);
// Gets: constructor, == by value, ToString, Deconstruct
var p = new Point(1, 2);
Console.WriteLine(p); // Point { X = 1, Y = 2 }

2. with Expression

'with' creates a new record with specified properties changed, leaving others unchanged — equivalent to TypeScript spread.

TS
TypeScript
const p2 = { ...p, x: 10 };
C#
C#
var p2 = p with { X = 10 };
Console.WriteLine(p2); // Point { X = 10, Y = 2 }
Console.WriteLine(p);  // original unchanged

3. Switch Expression

switch expression returns a value and must be exhaustive. Pattern arms use property patterns, type patterns, and when guards.

TS
TypeScript
switch (s.kind) {
  case 'circle': return Math.PI * s.radius**2;
  ...default: exhaustive check
}
C#
C#
double area = shape switch {
  Circle  { Radius: var r }     => Math.PI * r * r,
  Rect    { Width: var w, Height: var h } => w * h,
  _                              => 0,
};
// Compiler warns if all cases aren't covered

4. Type and Guard Patterns

Combine type patterns, property patterns, and 'when' guards for expressive control flow.

TS
TypeScript
if (x instanceof Error && x.code === 404) { ... }
C#
C#
string Describe(object obj) => obj switch {
  string s when s.Length == 0 => "empty string",
  string s                    => $"string: {s}",
  int n when n < 0            => "negative",
  int n                       => $"int: {n}",
  null                        => "null",
  _                           => "other",
};
Rule of Thumb
Prefer record over class for data that is immutable, compared by value, and printed often (DTOs, value objects, domain events).

Common Mistakes

When coming from TypeScript, developers often make these mistakes:

  • record types have value-based equality by default (== compares properties) — unlike classes where == is reference equality
  • with expression creates a copy with specified properties changed — equivalent to object spread in TypeScript
  • switch expression (not statement) with => returns a value; exhaustiveness checked by compiler
Common Pitfall
Don't assume C# works exactly like TypeScript. While the concepts may be similar, the syntax and behavior can differ significantly.

Key Takeaways

  • record Point(X, Y) — auto-generates constructor, value equality, ToString, with support
  • p with { X = 10 } creates a new record with X changed — non-destructive update like spread
  • switch expression with property patterns is exhaustive and returns a value
  • Type equality for records is by property values (==); for classes it's reference equality
Rule of Thumb
The best way to learn is by doing. Try rewriting some of your TypeScript code in C# to practice these concepts.
PreviousFinish