The standard advice for picking up a new programming language is: find a tutorial, work through the basics, build something small. That works, but it’s slow, and most of it teaches you things you already know in a different syntax.
If you already know one language well, there’s a faster path. You don’t need to learn programming again, just a translation layer.
The approach I use is to take a language I know, take the one I want to learn, and ask an AI for a structured side-by-side reference comparing the two. One afternoon of prompting gives you something more useful than most beginner tutorials.
The prompt
The key is being specific. Don’t ask “teach me C#”; ask for a direct comparison against what you already know.
I know TypeScript well. I'm learning C#. Generate a comprehensive markdown
reference document comparing TypeScript and C# side by side.
Cover the following in order:
1. Variable declaration and type system
2. Functions and arrow functions / lambdas
3. Classes, interfaces, and inheritance
4. Generics
5. Null handling
6. Async / await and concurrency
7. Collections and array operations (map, filter, reduce equivalents)
8. Error handling
9. Modules / namespaces and imports
10. Common standard library equivalents (string manipulation, date handling, etc.)
For each section, show TypeScript on the left and C# on the right with a brief
explanation of key differences. Focus on what's different, not what's the same.
Format it as a markdown file I can save and reference locally.
That’s it. Run that once, save the output as ts-to-csharp.md, and you have a personal cheat sheet built from your exact knowledge baseline.
What the output looks like
Here’s a condensed version of what you get back, the kind of reference that sticks because every entry maps to something you already understand.
Variable declaration
| Concept | TypeScript | C# |
|---|---|---|
| Inferred | const x = 42 |
var x = 42; |
| Explicit | const x: number = 42 |
int x = 42; |
| Mutable | let x = 42 |
var x = 42; |
| Immutable | const x = 42 |
const int x = 42; |
| Nullable | let x: number | null = null |
int? x = null; |
Key difference: const in TypeScript makes the binding immutable, not the value. In C#, const means a compile-time constant. For the TypeScript sense of a readonly reference, use readonly in C#.
Functions and lambdas
// TypeScript
function add(a: number, b: number): number {
return a + b;
}
const add = (a: number, b: number): number => a + b;
// optional param
function greet(name: string, greeting?: string) {
return `${greeting ?? "Hello"}, ${name}`;
}
// C#
int Add(int a, int b) {
return a + b;
}
Func<int, int, int> add = (a, b) => a + b;
// optional param
string Greet(string name, string greeting = "Hello") {
return $"{greeting}, {name}";
}
Key difference: TypeScript optional params use ?, while C# uses default values. C# lambdas are typed as Func<TIn, TOut> or Action<TIn>, which you’ll see a lot in LINQ.
Generics
// TypeScript
function identity<T>(value: T): T {
return value;
}
interface Box<T> {
value: T;
transform: (fn: (v: T) => T) => Box<T>;
}
// C#
T Identity<T>(T value) {
return value;
}
interface IBox<T> {
T Value { get; }
IBox<T> Transform(Func<T, T> fn);
}
Key difference: C# generics support constraints with where T : SomeClass, similar to TypeScript’s <T extends SomeType>. C# also has variance annotations (in/out) on interfaces.
Null handling
// TypeScript
const user = getUser(); // User | null
const name = user?.name ?? "Anonymous";
// type narrowing
if (user !== null) {
console.log(user.name); // user: User here
}
// C#
User? user = GetUser(); // nullable reference type
string name = user?.Name ?? "Anonymous";
// pattern matching
if (user is User u) {
Console.WriteLine(u.Name);
}
Key difference: C# nullable reference types (string?) are opt-in per project, enabled with <Nullable>enable</Nullable> in your .csproj. Pattern matching with is is idiomatic C# and more powerful than TypeScript’s type narrowing.
Async / await
// TypeScript
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/users/${id}`);
if (!res.ok) throw new Error("Not found");
return res.json();
}
// parallel
const [a, b] = await Promise.all([fetchUser(1), fetchUser(2)]);
// C#
async Task<User> FetchUserAsync(int id) {
var res = await httpClient.GetAsync($"/users/{id}");
if (!res.IsSuccessStatusCode) throw new Exception("Not found");
return await res.Content.ReadFromJsonAsync<User>();
}
// parallel
var (a, b) = await (FetchUserAsync(1), FetchUserAsync(2)).WhenAll();
// or more commonly:
var results = await Task.WhenAll(FetchUserAsync(1), FetchUserAsync(2));
Key difference: TypeScript uses Promise<T> and C# uses Task<T>, but the pattern is identical. By convention, C# async methods get an Async suffix. C# also has ValueTask<T> for performance-sensitive paths where allocation matters.
Array operations
// TypeScript
const nums = [1, 2, 3, 4, 5];
nums.filter((n) => n > 2); // [3, 4, 5]
nums.map((n) => n * 2); // [2, 4, 6, 8, 10]
nums.reduce((acc, n) => acc + n, 0); // 15
nums.find((n) => n > 3); // 4
nums.every((n) => n > 0); // true
nums.some((n) => n > 4); // true
// C#
var nums = new[] { 1, 2, 3, 4, 5 };
nums.Where(n => n > 2); // [3, 4, 5]
nums.Select(n => n * 2); // [2, 4, 6, 8, 10]
nums.Aggregate(0, (acc, n) => acc + n); // 15
nums.FirstOrDefault(n => n > 3); // 4
nums.All(n => n > 0); // true
nums.Any(n => n > 4); // true
Key difference: LINQ in C# is the equivalent of lodash plus the native array methods. Results are lazy by default (IEnumerable<T>), so call .ToList() or .ToArray() to materialise them. This trips up almost everyone coming from TypeScript.
Error handling
// TypeScript
try {
const data = JSON.parse(raw);
} catch (err) {
if (err instanceof SyntaxError) {
console.error("Bad JSON:", err.message);
} else {
throw err;
}
}
// C#
try {
var data = JsonSerializer.Deserialize<MyType>(raw);
} catch (JsonException ex) {
Console.Error.WriteLine($"Bad JSON: {ex.Message}");
} catch (Exception ex) {
throw; // rethrow preserving stack trace
}
Key difference: C# catch blocks can be typed directly (catch (JsonException ex)), with no need for instanceof checks. Use a bare throw; (not throw ex;) to rethrow without losing the original stack trace.
Imports and namespaces
// TypeScript
import { readFile } from "fs/promises";
import type { User } from "./types";
export { User };
export default function main() {}
// C#
using System.IO;
using MyApp.Models; // User lives here
// no "default export" — everything is in a namespace
namespace MyApp.Services;
public class MainService { }
Key difference: C# has no default exports, and everything lives in a namespace. Modern C# (10+) supports file-scoped namespaces (namespace MyApp.Services;), which removes a level of indentation. using static lets you import static members directly, similar to named imports.
Why this works better than tutorials
A tutorial has to assume you know nothing, so it spends time on if statements and loops. The comparison document skips all that and assumes you understand the concept and just need the syntax and the gotchas.
The parts that actually slow you down in a new language aren’t the fundamentals. They’re the subtle differences: that LINQ is lazy, that C# const means something different, that throw; and throw ex; behave differently. A comparison document surfaces exactly those gaps.
Iterate on it
The first pass covers the basics. Once you’ve been writing C# for a week, come back and ask for a second document:
I've been writing C# for a week coming from TypeScript.
Generate a follow-up reference covering more advanced topics:
- Records vs TypeScript type aliases
- Pattern matching (switch expressions, is patterns)
- Spans and memory management basics
- Dependency injection conventions
- The difference between IEnumerable, ICollection, IList, and List
Each iteration targets exactly where you are. You aren’t re-reading things you’ve already absorbed; you’re filling in the next layer of gaps.
Save it, version it, share it
The markdown file you get back is a real artifact. Commit it to your dotfiles or a references/ folder in your project, update it as you learn, and share it with teammates making the same move.
It beats bookmarking a tutorial you’ll never finish, and it’s more honest than pretending you don’t need to look things up.