Classes: Advanced Features
Classes & OOP
Introduction
In this lesson, you'll learn about classes: advanced features 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.
In TypeScript, you're familiar with classes & oop.
C# has its own approach to classes & oop, which we'll explore step by step.
The C# Way
Let's see how C# handles this concept. Here's a typical example:
// C# 9 record — immutable data class
record User(int Id, string Name);
// C# 12 — primary constructors
class Product(string Sku, string Name, decimal Price) {
public string Sku { get; } = Sku;
public string Name { get; set; } = Name;
private decimal Price { get; } = Price;
}
// C# 9 init-only setters
class Config {
public string Host { get; init; } = "localhost";
public int Port { get; init; } = 8080;
}
// Object initializer with init
var cfg = new Config { Host = "prod.example.com", Port = 443 };Comparing to TypeScript
Here's how you might have written similar code in TypeScript:
class User {
readonly id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
// TypeScript 5 — parameter properties
class Product {
constructor(
public readonly sku: string,
public name: string,
private price: number,
) {}
}You may be used to different syntax or behavior.
C# record provides immutable value-semantic classes with less boilerplate
You may be used to different syntax or behavior.
C# init-only setters allow construction-time assignment but prevent later mutation
You may be used to different syntax or behavior.
C# primary constructors (C# 12) are similar to TypeScript parameter properties
You may be used to different syntax or behavior.
required keyword in C# enforces that a property is set during construction
Step-by-Step Breakdown
1. record = Immutable Data Class
C# record generates constructor, ToString, Equals, GetHashCode, and deconstruction automatically. It's the C# equivalent of TypeScript readonly interfaces with less code.
interface User { readonly id: number; readonly name: string; }record User(int Id, string Name);
// Auto-generates: equality, ToString, deconstruct
var u = new User(1, "Alice");
var (id, name) = u; // deconstruction2. init-only Properties
init replaces set in property definitions. The value can only be assigned at construction time (new MyClass { Prop = value }) — cannot be changed after.
class Config {
readonly host: string = "localhost";
}class Config {
public string Host { get; init; } = "localhost";
}
// OK:
var c = new Config { Host = "prod" };
// Error:
// c.Host = "other"; // init-only!3. required Properties
The required keyword forces callers to set the property in an object initializer. The compiler errors if it's missing — similar to TypeScript's required fields.
interface CreateUser { name: string; email: string; } // all requiredclass CreateUserDto {
public required string Name { get; init; }
public required string Email { get; init; }
}
// new CreateUserDto() // Error: Name, Email required4. with Expression — Non-Destructive Mutation
Records support the with expression to create a copy with some properties changed — equivalent to TypeScript's spread: { ...obj, name: 'new' }.
const updated = { ...user, name: "Bob" };var updated = user with { Name = "Bob" };
// Original user is unchangedCommon Mistakes
When coming from TypeScript, developers often make these mistakes:
- C# record provides immutable value-semantic classes with less boilerplate
- C# init-only setters allow construction-time assignment but prevent later mutation
- C# primary constructors (C# 12) are similar to TypeScript parameter properties
Key Takeaways
- C# record is the go-to for immutable data — generates equality, ToString, and deconstruction
- init-only setters allow construction-time assignment, preventing later mutation
- required properties enforce that callers must set them in object initializers
- with expression creates record copies with changed properties (like spread in TS)