The Fastest Way to Learn a New Programming Language: Use One You Already Know

Stop reading documentation from scratch. Ask AI to generate a side-by-side comparison between a language you know and the one you're learning — then just read the diff.

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 is teaching 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. You need a translation layer.

Here’s the approach I use: take a language you know, take the one you want to learn, and ask an AI to generate a structured side-by-side reference document 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 document that actually sticks because every entry maps to something you already understand.


Variable declaration

ConceptTypeScriptC#
Inferredconst x = 42var x = 42;
Explicitconst x: number = 42int x = 42;
Mutablelet x = 42var x = 42;
Immutableconst x = 42const int x = 42;
Nullablelet x: number | null = nullint? x = null;

Key difference: const in TypeScript means the binding is immutable, not the value. In C#, const means compile-time constant. For the TypeScript equivalent 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 ?, C# uses default values. C# lambdas are typed as Func<TIn, TOut> or Action<TIn> — you’ll see these 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 in <Nullable>enable</Nullable> in your .csproj). Pattern matching with is is idiomatic C# — 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>, C# uses Task<T>. The pattern is identical. By convention, C# async methods are suffixed with Async. 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 + native array methods. Results are lazy by default (IEnumerable<T>) — 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)) — no need for instanceof checks. Use 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. Everything lives in a namespace. Modern C# (10+) supports file-scoped namespaces (namespace MyApp.Services;) which removes one 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. It spends time on if statements and loops. The comparison document skips all of that — it starts from the assumption that you understand the concept and just need the syntax and the gotchas.

The parts that actually slow you down when learning a new language aren’t the fundamentals. They’re the subtle differences: the fact that LINQ is lazy, that C# const means something different, that throw; and throw ex; have different semantics. 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’re not 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 first-class artifact. Commit it to your dotfiles or a references/ folder in your project. Update it as you learn. Share it with teammates making the same transition.

It’s more useful than bookmarking a tutorial you’ll never finish, and more honest than pretending you don’t need to look things up.