PONY λ M2 Modula-2

TypeScript.CodeCompared.To/C#

An interactive executable cheatsheet comparing TypeScript and C#

TypeScript 6.0 C# 13
Hello World & Running
Hello, World
console.log("Hello, World!");
Console.WriteLine("Hello, World!");
C# 9 introduced top-level statements, so a script needs no class or Main method — the same shape as a TypeScript file run directly. Output goes through Console.WriteLine, which appends a newline for you.
String interpolation
const name = "Ada"; console.log(`Hello, ${name}!`);
var name = "Ada"; Console.WriteLine($"Hello, {name}!");
C# interpolates with the $"..." prefix and single braces {name} — no backtick or $-before-brace. The expression inside the braces is fully type-checked, just like a TypeScript template literal.
Variables & Types
var and explicit types
let count: number = 3; const label = "items"; console.log(count, label);
int count = 3; var label = "items"; Console.WriteLine($"{count} {label}");
C# var infers the type like an untyped let, but the type is fixed once and checked at compile time — there is no any. The explicit form puts the type first: int count rather than count: number.
Value vs reference types
const a = { x: 1 }; const b = a; b.x = 9; console.log(a.x);
var first = (X: 1, Y: 2); var second = first; // tuple is a value type — copied second.X = 9; Console.WriteLine(first.X);
C# distinguishes struct value types (copied on assignment) from class reference types (shared), where every JavaScript object is a shared reference. Tuples and the built-in numbers are value types, so second here is an independent copy.
const and readonly
const maxItems = 100; console.log(maxItems);
const int MaxItems = 100; Console.WriteLine(MaxItems);
C# const is a true compile-time constant and must be a literal known at compile time — stricter than TypeScript's const, which only forbids rebinding. For a value computed once at runtime, C# uses readonly instead.
Strings
String operations
const text = "Hello, World"; console.log(text.toUpperCase()); console.log(text.includes("World")); console.log(text.length);
var text = "Hello, World"; Console.WriteLine(text.ToUpper()); Console.WriteLine(text.Contains("World")); Console.WriteLine(text.Length);
C# method and property names are PascalCase, so it is ToUpper and Length. Strings are immutable reference types, as in TypeScript, and Length is a property, not a method call.
The char type
const text = "hello"; console.log(text[0]); console.log(text[0] === "h");
var text = "hello"; char first = text[0]; Console.WriteLine(first); Console.WriteLine(first == 'h');
Indexing a C# string yields a char, a distinct 16-bit value type written with single quotes ('h') — not a one-character string as in TypeScript. Single and double quotes are not interchangeable.
Numbers
Numeric types
const count = 3; const ratio = 1.5; console.log(count, ratio);
int count = 3; double ratio = 1.5; decimal price = 19.99m; Console.WriteLine($"{count} {ratio} {price}");
C# replaces TypeScript's single number with distinct types: int, long, double, and decimal for exact money math (note the m suffix). The type is part of the contract and overflow can be checked.
Integer division
console.log(7 / 2); console.log(Math.floor(7 / 2));
Console.WriteLine(7 / 2); Console.WriteLine(7 / 2.0);
Dividing two int values truncates — 7 / 2 is 3, where TypeScript always gives 3.5. Making one operand a double (2.0) restores fractional division. The static types decide the behavior.
Lists & Arrays
Lists
const numbers: number[] = [1, 2, 3]; numbers.push(4); console.log(numbers[0], numbers.length);
var numbers = new List<int> { 1, 2, 3 }; numbers.Add(4); Console.WriteLine($"{numbers[0]} {numbers.Count}");
The growable list is List<T> — every element shares the declared type, so no mixed-type arrays. You Add rather than push, and the size is the Count property. A fixed-size int[] array also exists for low-level work.
Collection expressions
const first = [1, 2, 3]; const more = [...first, 4, 5]; console.log(more);
int[] first = [1, 2, 3]; int[] more = [..first, 4, 5]; Console.WriteLine(string.Join(", ", more));
C# 12 added collection expressions with [...] literals and a spread operator written .. (two dots, not three). It is the modern, terse way to build arrays and lists that reads much like TypeScript's array spread.
Dictionaries
Dictionaries
const ages: Record<string, number> = {}; ages["ada"] = 36; console.log(ages["ada"]);
var ages = new Dictionary<string, int>(); ages["ada"] = 36; Console.WriteLine(ages["ada"]);
Dictionary<TKey, TValue> is the typed key/value store — both types are fixed, unlike a TypeScript object that doubles as a record and a map. Indexing a missing key throws rather than returning undefined.
Safe lookup
const ages: Record<string, number> = { ada: 36 }; const value = ages["missing"]; console.log(value ?? "absent");
var ages = new Dictionary<string, int> { ["ada"] = 36 }; if (ages.TryGetValue("missing", out var value)) Console.WriteLine(value); else Console.WriteLine("absent");
Because a missing key throws, C# offers TryGetValue, which returns a bool and writes the result into an out variable. The out var value declares the variable inline — a pattern with no TypeScript equivalent.
Control Flow
foreach
for (const value of [10, 20, 30]) { console.log(value); }
foreach (var value in new[] { 10, 20, 30 }) { Console.WriteLine(value); }
C# spells the iterate-over-values loop foreach (var x in items), the direct analogue of for...of. A classic for (int i = 0; ...) loop also exists when you need the index.
switch expressions
const day = 6; const kind = (day === 0 || day === 6) ? "weekend" : "weekday"; console.log(kind);
var day = 6; var kind = day switch { 0 or 6 => "weekend", _ => "weekday", }; Console.WriteLine(kind);
The switch expression returns a value and uses => arms with no break. Patterns like 0 or 6 and a _ catch-all make it far more capable than a TypeScript switch statement, and the compiler warns if cases are unhandled.
Methods & Lambdas
Methods & local functions
function add(a: number, b: number): number { return a + b; } console.log(add(2, 3));
int Add(int a, int b) { return a + b; } Console.WriteLine(Add(2, 3));
A function declared among top-level statements is a local function. The return type comes first (int Add) rather than after the parameters, and every parameter is typed. An explicit return is required.
Lambdas
const square = (x: number): number => x * x; console.log(square(5));
Func<int, int> square = x => x * x; Console.WriteLine(square(5));
C# lambdas use the same => arrow, but a lambda stored in a variable needs a delegate type: Func<int, int> (last type parameter is the return) or Action<T> for one returning nothing.
Optional & named arguments
function greet(name: string, greeting = "Hello"): string { return `${greeting}, ${name}`; } console.log(greet("Ada"));
string Greet(string name, string greeting = "Hello") { return $"{greeting}, {name}"; } Console.WriteLine(Greet("Ada"));
C# supports default parameter values directly in the signature, and callers may also pass arguments by name (Greet("Ada", greeting: "Hi")) — real named arguments rather than the options-object pattern TypeScript leans on.
Classes & Properties
Classes
class Point { constructor(public x: number, public y: number) {} distance(): number { return Math.sqrt(this.x ** 2 + this.y ** 2); } } console.log(new Point(3, 4).distance());
var point = new Point(3, 4); Console.WriteLine(point.Distance()); class Point(double x, double y) { public double Distance() => Math.Sqrt(x * x + y * y); }
C# 12 primary constructorsclass Point(double x, double y) — capture parameters for use in the body, mirroring TypeScript's constructor(public x) shorthand. Instantiation uses new, and a one-line method can use the => expression body.
Properties
class Counter { value = 0; } const counter = new Counter(); counter.value = 5; console.log(counter.value);
var counter = new Counter(); counter.Value = 5; Console.WriteLine(counter.Value); class Counter { public int Value { get; set; } }
What looks like a field is a C# property: { get; set; } generates a getter and setter, so you can later add logic without changing callers. This auto-property pattern is the idiomatic replacement for a public field.
Inheritance
class Animal { speak(): string { return "..."; } } class Dog extends Animal { override speak(): string { return "Woof"; } } console.log(new Dog().speak());
Console.WriteLine(new Dog().Speak()); class Animal { public virtual string Speak() => "..."; } class Dog : Animal { public override string Speak() => "Woof"; }
C# uses a colon (Dog : Animal) for inheritance. Overriding is explicit on both sides: the base method must be marked virtual and the override marked override — stricter than TypeScript, where any same-named method silently overrides.
Records
Records & value equality
interface Point { readonly x: number; readonly y: number; } const a: Point = { x: 1, y: 2 }; const b: Point = { x: 1, y: 2 }; console.log(a === b); console.log(a.x === b.x && a.y === b.y);
var a = new Point(1, 2); var b = new Point(1, 2); Console.WriteLine(a == b); Console.WriteLine(a); record Point(int X, int Y);
A record is an immutable data type with value equality built in — a == b is true when the fields match, where TypeScript's === only compares references. Records also get a readable ToString and a with expression for copies, for free.
Non-destructive mutation
const point = { x: 1, y: 2 }; const moved = { ...point, y: 9 }; console.log(moved);
var point = new Point(1, 2); var moved = point with { Y = 9 }; Console.WriteLine(moved); record Point(int X, int Y);
The with expression makes a copy of a record changing only the named fields — the typed, immutable counterpart of TypeScript's object spread { ...point, y: 9 }. The original record is never modified.
Interfaces
Interfaces
interface Speaker { speak(): string; } class Dog implements Speaker { speak(): string { return "Woof"; } } const announce = (s: Speaker) => s.speak(); console.log(announce(new Dog()));
string Announce(ISpeaker speaker) => speaker.Speak(); Console.WriteLine(Announce(new Dog())); interface ISpeaker { string Speak(); } class Dog : ISpeaker { public string Speak() => "Woof"; }
C# interfaces are nominal, not structural: a type must explicitly list the interface (Dog : ISpeaker) to satisfy it, whereas in TypeScript any object of the right shape qualifies. By convention interface names start with I.
Pattern Matching
Type patterns
function describe(value: unknown): string { if (typeof value === "number") return `number ${value}`; if (typeof value === "string") return `string ${value}`; return "other"; } console.log(describe(42));
string Describe(object value) => value switch { int number => $"number {number}", string text => $"string {text}", _ => "other", }; Console.WriteLine(Describe(42));
A C# switch can match on type and bind the narrowed value in one arm — int number both tests and names it. This is the typed counterpart of narrowing with typeof, and it works on the object base type that stands in for unknown.
Property patterns
type Shape = | { kind: "circle"; radius: number } | { kind: "rect"; w: number; h: number }; function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "rect": return shape.w * shape.h; } } console.log(area({ kind: "rect", w: 3, h: 4 }));
double Area(Shape shape) => shape switch { Circle { Radius: var r } => Math.PI * r * r, Rect { W: var w, H: var h } => w * h, }; Console.WriteLine(Area(new Rect(3, 4))); abstract record Shape; record Circle(double Radius) : Shape; record Rect(double W, double H) : Shape;
C# property patterns destructure and match in a switch arm, and a sealed record hierarchy gives exhaustiveness much like a TypeScript discriminated union — but the matching is on the actual type, not a literal kind tag you maintain by hand.
Nullability
Nullable reference types
let name: string | null = null; console.log(name ?? "anonymous"); name = "Ada"; console.log(name.toUpperCase());
string? name = null; Console.WriteLine(name ?? "anonymous"); name = "Ada"; Console.WriteLine(name.ToUpper());
C#'s nullable reference types make string non-null by default and string? explicitly nullable — exactly TypeScript's strictNullChecks model. The ?? null-coalescing operator is shared, and the compiler warns before you dereference a possibly-null value.
Null-conditional access
const user: { name?: string } = {}; console.log(user.name?.toUpperCase() ?? "none");
string? name = null; Console.WriteLine(name?.ToUpper() ?? "none");
The ?. null-conditional operator short-circuits to null instead of throwing — the very operator TypeScript borrowed. Combined with ??, it reads identically across the two languages.
Generics
Generic methods
function first<T>(items: T[]): T { return items[0]; } console.log(first([10, 20, 30]));
T First<T>(IEnumerable<T> items) => items.First(); Console.WriteLine(First(new[] { 10, 20, 30 }));
Generics use the same <T> angle brackets as TypeScript. The crucial difference is that C# generics are reified — the type argument is real at runtime, so typeof(T) works — whereas TypeScript erases all type parameters during compilation.
Generic constraints
function maxOf<T>(a: T, b: T, greater: (x: T, y: T) => boolean): T { return greater(a, b) ? a : b; } console.log(maxOf(3, 9, (x, y) => x > y));
T MaxOf<T>(T a, T b) where T : IComparable<T> => a.CompareTo(b) > 0 ? a : b; Console.WriteLine(MaxOf(3, 9));
A where T : IComparable<T> clause constrains the type parameter to types that implement an interface, so the body may call CompareTo. The constraint is checked at compile time and is more expressive than TypeScript's extends bound.
LINQ
LINQ vs array methods
const numbers = [1, 2, 3, 4, 5, 6]; const result = numbers .filter(n => n % 2 === 0) .map(n => n * 10); console.log(result);
var numbers = new[] { 1, 2, 3, 4, 5, 6 }; var result = numbers .Where(n => n % 2 == 0) .Select(n => n * 10) .ToList(); Console.WriteLine(string.Join(", ", result));
LINQ's Where/Select are C#'s filter/map, but they are lazy — the pipeline does not run until enumerated, which is why a terminal ToList() forces it. The methods come from System.Linq and work over any IEnumerable<T>.
Aggregation
const numbers = [1, 2, 3, 4]; const total = numbers.reduce((sum, n) => sum + n, 0); const anyBig = numbers.some(n => n > 3); console.log(total, anyBig);
var numbers = new[] { 1, 2, 3, 4 }; var total = numbers.Sum(); var anyBig = numbers.Any(n => n > 3); Console.WriteLine($"{total} {anyBig}");
LINQ ships dedicated aggregators — Sum, Average, Min, Max, Count — instead of expressing everything through reduce. Any and All replace some and every.
Async & Tasks
async/await
async function compute(): Promise<number> { return 42; } (async () => { const value = await compute(); console.log(value); })();
async Task<int> Compute() { await Task.Delay(1); return 42; } var value = await Compute(); Console.WriteLine(value);
C# pioneered the async/await syntax TypeScript later adopted. A Task<T> is the C# Promise<T>, and top-level statements allow await directly — but unlike JavaScript's single event loop, C# tasks can run on real thread-pool threads.
Error Handling
try/catch/finally
try { throw new Error("boom"); } catch (error) { console.log((error as Error).message); } finally { console.log("cleanup"); }
try { throw new Exception("boom"); } catch (Exception error) { Console.WriteLine(error.Message); } finally { Console.WriteLine("cleanup"); }
The keywords match TypeScript, but C# exceptions are strongly typed: you catch a specific Exception subclass, and the caught variable has a known type and Message property — no as Error cast or unknown to narrow.
Custom exceptions
class ValidationError extends Error {} try { throw new ValidationError("invalid"); } catch (error) { if (error instanceof ValidationError) console.log("caught"); }
try { throw new ValidationException("invalid"); } catch (ValidationException) { Console.WriteLine("caught"); } class ValidationException : Exception { public ValidationException(string message) : base(message) { } }
A custom exception subclasses Exception, and a typed catch (ValidationException) handles only that type — the compiler-checked equivalent of an instanceof guard inside a single catch block.