From JavaScript/TypeScript to C#: A Developer’s Migration Guide

Are you a JavaScript/TypeScript developer looking to make the move to C# and the .NET ecosystem? Maybe you want to work in enterprise environments, build robust web applications, or leverage the power of C# for game development with Unity.

Whatever your motivation, transitioning from dynamic, interpreted languages like JavaScript and TypeScript to a statically-typed, compiled language like C# can be a significant shift. You may look at C# and think: why do I have to declare every variable type? Why do I need to compile my code before running it? Why does everything have to be so strict?

But let’s be honest, while TypeScript has fixed many of the issues with JavaScript’s dynamic typing, it can often be a frustrating experience with its complex type system and sometimes confusing error messages. Your code works fine, and it’s nearly time to submit a pull request when suddenly a wild no overload matches this call error appears. You fix it, but that creates another error in a different file. You fix that, and then another error appears in a third file—and now it’s one hour later and you still haven’t finished your pull request. Well, good news: none of that will happen in C#. The compiler will catch those errors before you even run your code, so you won’t have ‘code that works’ that you can’t commit.

Why C#?

C# is a powerful, modern programming language that combines the best features of C-type languages with a rich ecosystem for building everything from web applications to desktop software and cloud services. With its strong type system, robust tooling, and extensive libraries, C# is an excellent choice for developers looking to build scalable, maintainable applications.

As a JavaScript/TypeScript developer, you already have a solid foundation in programming concepts, which will make your transition to C# smoother. A lot of the syntax and programming paradigms will feel familiar, but there are key differences that you’ll need to understand to be effective in C#.

The .NET Ecosystem

C# is part of the broader .NET ecosystem, which provides a comprehensive platform for building various types of applications. .NET supports multiple programming paradigms beyond object-oriented programming:

Multi-Paradigm Support: .NET and C# embrace functional programming concepts like immutability, higher-order functions, and pattern matching, alongside traditional object-oriented programming. This flexibility allows you to choose the most appropriate paradigm for each problem, similar to how JavaScript supports both functional and object-oriented styles.

Cross-Platform Development: Modern .NET runs on Windows, macOS, and Linux, making it a truly cross-platform solution for your applications.

Diverse Application Types: You can build web APIs with ASP.NET Core, desktop applications with WPF or WinUI, mobile apps with .NET MAUI, cloud-native applications, microservices, and even games with Unity.

ASP.NET Core: Coming from web development, you’ll particularly appreciate ASP.NET Core, Microsoft’s modern, cross-platform framework for building web applications and APIs. It shares many concepts with Node.js frameworks like Express, including middleware pipelines, dependency injection, and RESTful routing, making the transition smoother for web developers.

Blazor for Web Development: If you’re building a web application, consider using Blazor to build your application. Blazor is a full-stack web framework built for .NET and C#. Blazor components can run on the server, as .NET assemblies, or on the client using WebAssembly. Blazor supports interop with your favorite JavaScript or TypeScript libraries, making it an excellent bridge technology for JS/TS developers moving to C#.

Interpreted vs Compiled Languages

JavaScript is primarily an interpreted language, meaning that code is executed line-by-line at runtime. This allows for rapid development and testing but can lead to runtime errors that are only caught when the code is executed. C#, on the other hand, is a compiled language. This means that your code is transformed into an intermediate language (IL) before it runs, allowing the compiler to catch many errors at compile time. This shift from interpreted to compiled can significantly improve code reliability and performance.

Understanding C-Type Languages

C# belongs to the C family of languages, which includes C, C++, Java, and many others. These languages share common ancestry and design principles that make them distinct from JavaScript:

Core Characteristics of C-Type Languages

Static Typing: Variables must be declared with specific types at compile time. The compiler catches type errors before your code runs, unlike JavaScript’s runtime type checking.

Compilation: C# code is compiled to bytecode (IL - Intermediate Language) before execution, providing performance benefits and early error detection.

Memory Management: While C# has garbage collection like JavaScript, it gives you more control over memory allocation and object lifecycles.

Strong Type System: C# enforces strict type rules with explicit casting requirements, preventing many runtime errors common in JavaScript.

Object-Oriented Foundation: C# was designed from the ground up as an object-oriented language, with features like inheritance, polymorphism, and encapsulation as first-class citizens. However, it also supports functional programming paradigms, pattern matching, and immutable data structures.

Multi-Paradigm Flexibility: Modern C# (C# 9.0+) includes features like record types for immutable data, pattern matching for functional-style programming, and comprehensive LINQ support that enables declarative programming approaches familiar to JavaScript developers.

JavaScript/TypeScript vs C#: Similarities That Help You Transition

The good news is that JavaScript, TypeScript, and C# are all members of the C family of languages. These similarities help you quickly become productive in C#:

Shared Language Features

Similar Syntax: The curly braces and semicolons are familiar. Control statements like if, else, switch are the same. Looping statements of for, while, and do...while are identical. The same keywords for class and interface exist in both C# and TypeScript.

Access Modifiers: TypeScript and C# share the same access modifiers, from public to private.

The => Token: All languages support lightweight function definitions. In C#, they’re referred to as lambda expressions; in JavaScript, they’re typically called arrow functions.

Function Hierarchies: All three languages support local functions—functions defined within other functions.

Async/Await: All three languages share the same async and await keywords for asynchronous programming.

Garbage Collection: All three languages rely on a garbage collector for automatic memory management.

Event Model: C#’s event syntax is similar to JavaScript’s model for document object model (DOM) events.

Package Management: NuGet is the most common package manager for C# and .NET, similar to npm for JavaScript applications. C# libraries are delivered in assemblies.

Key Differences and New Concepts

Aspect JavaScript TypeScript C#
Type System Dynamic, runtime Static with inference Static, explicit
Compilation Interpreted/JIT Transpiled to JS Compiled to IL
Variable Declaration var, let, const var, let, const + types int, string, var, etc.
Function Syntax function name() {} function name(): type {} public void Name() {}
Classes ES6+ classes Classes with types Full OOP classes
Null Handling null, undefined null, undefined + strict null + nullable types
Arrays [] dynamic type[] or Array<type> int[] or List<int>
Error Handling try/catch try/catch try/catch/finally
Async Programming Promises/async-await Promises/async-await Task/async-await
Package Management npm/yarn npm/yarn NuGet
Runtime V8, Node.js V8, Node.js .NET Runtime
Entry Point Script execution Script execution Main() method

New Concepts Coming from JavaScript/TypeScript

As you learn C#, you’ll encounter concepts that aren’t part of JavaScript, though some might be familiar if you use TypeScript:

Strong Type System: C# is a strongly typed language. Every variable has a type, and that type can’t change. You define class or struct types and interface definitions that specify behavior implemented by other types. TypeScript includes many of these concepts, but because TypeScript is built on JavaScript, the type system isn’t as strict.

Pattern Matching: Pattern matching enables concise conditional statements and expressions based on the shape of complex data structures. The is expression checks if a variable “is” some pattern. The pattern-based switch expression provides rich syntax to inspect a variable and make decisions based on its characteristics.

// Pattern matching example
public string DescribeType(object obj) => obj switch
{
    int i when i > 0 => "Positive integer",
    int i when i < 0 => "Negative integer",
    int => "Zero",
    string s when string.IsNullOrEmpty(s) => "Empty string",
    string => "Non-empty string",
    _ => "Unknown type"
};

String Interpolation and Raw String Literals: String interpolation enables you to insert evaluated expressions in a string, rather than using positional identifiers. Raw string literals provide a way to minimize escape sequences in text.

// String interpolation
var name = "John";
var age = 25;
var message = $"Hello, {name}! You are {age} years old.";

// Raw string literals (C# 11+)
var json = """
{
    "name": "John",
    "age": 25,
    "active": true
}
""";

Nullable and Non-nullable Types: C# supports nullable value types and nullable reference types by appending the ? suffix to a type. For nullable types, the compiler warns you if you don’t check for null before dereferencing. For non-nullable types, the compiler warns you if you might be assigning a null value. These features minimize your application throwing a System.NullReferenceException. The syntax might be familiar from TypeScript’s use of ? for optional properties.

// Nullable reference types
string? nullableName = null; // Can be null
string nonNullableName = "John"; // Cannot be null

// Nullable value types
int? nullableNumber = null;
int nonNullableNumber = 42;

LINQ (Language Integrated Query): LINQ provides a common syntax to query and transform data, regardless of its storage. It’s similar to JavaScript’s array methods but more powerful and works across different data sources.

What’s Not Available in C#

Some familiar features from JavaScript and TypeScript aren’t available in C#:

Dynamic Types: C# uses static typing. A variable declaration includes the type, and that type can’t change. There’s a dynamic type in C# that provides runtime binding, but it’s used sparingly.

Prototypal Inheritance: C# inheritance is part of the type declaration. A C# class declaration states any base class. In JavaScript, you can set the __proto__ property to set the base type on any instance.

Union Types: C# doesn’t support union types like TypeScript. However, design proposals are in progress.

Decorators: C# doesn’t have decorators. Some common decorators, such as @sealed, are reserved keywords in C#. Other common decorators might have corresponding Attributes. For other decorators, you can create your own attributes.

More Forgiving Syntax: The C# compiler parses code more strictly than JavaScript requires.

Syntax Similarities and Differences

Variable Declaration

Compared to JavaScript/TypeScript, C# has a more explicit type declaration syntax, but the concepts of variables and constants are similar.

In JavaScript, you can declare variables using let, const, or less commonly var. You can assign values without specifying types, as JavaScript is dynamically typed. JavaScript uses type inference at runtime, meaning the type of a variable is determined when the code is executed.

// JavaScript
let name = "John";
const age = 25;
let isActive = true;

TypeScript is a superset of JavaScript that adds static types. In TypeScript, you can declare variables with types, but it still allows for dynamic typing. It essentially adds type annotations to JavaScript, allowing for better tooling and error checking at compile time. Note: As of Node.js v23.6.0 (January 2025), Node.js now supports TypeScript natively through experimental type stripping, allowing you to run .ts files directly without transpilation tools like ts-node.

// TypeScript
let name: string = "John";
const age: number = 25;
let isActive: boolean = true;

C# requires explicit type declarations for all variables. This means that every variable must have a type defined at compile time, and the compiler will enforce these types throughout the code.

// C#
string name = "John";
const int age = 25;
bool isActive = true;

// Or using type inference with 'var'
var inferredName = "John"; // Compiler infers string
var inferredAge = 25; // Compiler infers int

Functions and Methods

Here is the same function written in JavaScript, TypeScript, and C#:

// JavaScript
function calculateTotal(price, tax) {
  return price * (1 + tax);
}
// TypeScript
function calculateTotal(price: number, tax: number): number {
  return price * (1 + tax);
}
// C#
public int CalculateTotal(int price, double tax)
{
    return (int)(price * (1 + tax));
}

You can see that in JavaScript, the function is defined without type annotations, while in TypeScript, types are added for parameters and return values. In C#, the method signature includes access modifiers (like public), and types must be explicitly declared for both parameters and return values.

Lambda Functions vs Arrow Functions

Lambda expressions and arrow functions are one of the most similar features between JavaScript/TypeScript and C#, yet they have important conceptual differences that are worth understanding in detail.

What Are Lambda Expressions?

Lambda expressions derive from λ (lambda) calculus. Concisely put, lambda expressions are a compact way to create anonymous functions. These functions get treated as values that may be assigned to variables or passed to other functions. Both JavaScript and C# support this concept, though with different underlying implementations.

Syntax Comparison

The syntax is remarkably similar between the languages:

// JavaScript - Arrow Functions
const add = (a, b) => a + b;
const multiply = (x) => x * 2;
const greet = () => "Hello World";

// Multi-line with explicit return
const processData = (data) => {
  const processed = data.map((x) => x * 2);
  return processed.filter((x) => x > 10);
};
// TypeScript - Arrow Functions with Types
const add = (a: number, b: number): number => a + b;
const multiply = (x: number): number => x * 2;
const greet = (): string => "Hello World";

// Multi-line with explicit return
const processData = (data: number[]): number[] => {
  const processed = data.map((x) => x * 2);
  return processed.filter((x) => x > 10);
};
// C# - Lambda Expressions
Func<int, int, int> add = (a, b) => a + b;
Func<int, int> multiply = x => x * 2;
Func<string> greet = () => "Hello World";

// Multi-line with explicit return
Func<int[], int[]> processData = (data) => {
    var processed = data.Select(x => x * 2);
    return processed.Where(x => x > 10).ToArray();
};

Alternative: Anonymous Methods in C#

C# also supports anonymous methods using the delegate keyword, which is more verbose but functionally similar:

// C# Anonymous Methods (older syntax)
Func<int, int, int> add = delegate(int a, int b) { return a + b; };
Action<string> log = delegate(string message) { Console.WriteLine(message); };

// Can omit parameter list for Action delegates
Action greet = delegate { Console.WriteLine("Hello!"); };

Lambda expressions provide a more concise and expressive way to create an anonymous function compared to anonymous methods using the delegate operator.

Key Similarities

  1. Same Syntax Pattern: Both use the => operator (fat arrow)
  2. Expression vs Statement Forms: Both support single expressions and multi-statement blocks
  3. Type Inference: Both can infer types in many contexts
  4. Higher-Order Functions: Both can be passed as parameters and returned from functions
  5. Closures: Both can capture variables from their enclosing scope
// JavaScript - Capturing outer variables
function createCounter(start) {
  return () => ++start; // Captures 'start'
}
// C# - Capturing outer variables
Func<int> CreateCounter(int start) {
    return () => ++start; // Captures 'start'
}

Important Differences

1. Type System Integration

C# lambda expressions use the => operator and can be converted to delegate types like Func<> or Action<>, whereas JavaScript arrow functions are simply function objects.

// C# - Strong typing with delegates
Func<int, bool> isEven = x => x % 2 == 0;
Predicate<int> isPositive = x => x > 0;
Action<string> print = x => Console.WriteLine(x);
// JavaScript - All functions are just functions
const isEven = (x) => x % 2 === 0;
const isPositive = (x) => x > 0;
const print = (x) => console.log(x);

2. this Binding Behavior

Arrow functions in JavaScript do not have their own this context and inherit it from the enclosing scope, while C# lambda expressions can capture variables from their enclosing scope but don’t have the same this binding issues.

// JavaScript - 'this' binding difference
class MyClass {
  constructor() {
    this.value = 42;
  }

  regularMethod() {
    setTimeout(function () {
      console.log(this.value); // undefined - 'this' is not MyClass
    }, 100);

    setTimeout(() => {
      console.log(this.value); // 42 - arrow function preserves 'this'
    }, 100);
  }
}
// C# - No 'this' binding issues with lambdas
public class MyClass
{
    private int value = 42;

    public void RegularMethod()
    {
        Task.Delay(100).ContinueWith(_ => {
            Console.WriteLine(value); // Always accessible
        });
    }
}

3. Expression Trees

In C#, lambda expressions can be converted to expression trees, which represent code as data structures that can be analyzed and manipulated at runtime. This is particularly powerful for LINQ providers:

// C# - Lambda as Expression Tree
Expression<Func<int, bool>> predicate = x => x > 5;
// This can be analyzed, modified, and translated (e.g., to SQL)

// JavaScript has no direct equivalent - functions are opaque
const predicate = x => x > 5; // Cannot inspect its internal logic

4. Limitations and Capabilities

JavaScript arrow functions have specific limitations:

  • Cannot be used as constructors with the new keyword
  • Do not have their own arguments object
  • Cannot be used as generator functions

C# lambda expressions have different considerations:

  • Can be converted to delegates or expression trees
  • Support static modifier to prevent capturing local variables
  • Can have attributes applied to them for analysis tools

Practical Usage Examples

Array/Collection Processing:

// JavaScript
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((x) => x * 2);
const evens = numbers.filter((x) => x % 2 === 0);
const sum = numbers.reduce((acc, x) => acc + x, 0);
// C# with LINQ
var numbers = new[] { 1, 2, 3, 4, 5 };
var doubled = numbers.Select(x => x * 2);
var evens = numbers.Where(x => x % 2 == 0);
var sum = numbers.Aggregate(0, (acc, x) => acc + x);

Event Handling:

// JavaScript
button.addEventListener("click", () => {
  console.log("Button clicked!");
});
// C#
button.Click += (sender, e) => {
    Console.WriteLine("Button clicked!");
};

Control Structures

Control structures like if statements and loops are similar across these languages, but with some syntax differences.

// JavaScript/TypeScript
if (user.isActive) {
  console.log("User is active");
} else {
  console.log("User is inactive");
}

for (let i = 0; i < items.length; i++) {
  console.log(items[i]);
}
// C#
if (user.IsActive)
{
    Console.WriteLine("User is active");
}
else
{
    Console.WriteLine("User is inactive");
}

for (int i = 0; i < items.Length; i++)
{
    Console.WriteLine(items[i]);
}

Key Mental Shifts for JavaScript/TypeScript Developers

1. Embrace Static Typing

While TypeScript introduced you to types, C# makes them mandatory and more powerful. Every variable, parameter, and return value must have a declared type.

2. Compilation is Your Friend

Unlike JavaScript’s runtime errors, C# catches many errors at compile time. This shift from “fail fast at runtime” to “fail fast at compile time” improves code reliability.

3. Memory and Performance Awareness

C# gives you more control over memory usage and performance optimization. Understanding value types vs reference types becomes important.

4. Method Signatures Matter

In C#, method overloading based on parameter types is common and powerful. The same method name can have different implementations based on its parameters.

5. Namespace Organization

C# uses namespaces similar to TypeScript modules, but they’re more integral to the language’s organization system.

What You’ll Love About C#

IntelliSense and Tooling: Visual Studio and VS Code provide exceptional autocomplete and error detection thanks to static typing.

Performance: Compiled C# applications typically run faster than JavaScript, especially for CPU-intensive tasks.

Enterprise Features: Built-in support for dependency injection, configuration management, and enterprise patterns.

LINQ: Language Integrated Query provides powerful data manipulation capabilities that feel familiar to JavaScript array methods but are more powerful.

LINQ provides methods for filtering, aggregation, concatenation, and much more that are similar to JavaScript’s array methods but with different names and more powerful capabilities. Here are some common equivalents:

JavaScript Method C# LINQ Equivalent Purpose
map() Select() Transform each element
filter() Where() Filter elements by condition
reduce() Aggregate() Reduce to single value
find() FirstOrDefault() Find first matching element
some() Any() Check if any element matches
every() All() Check if all elements match
includes() Contains() Check if element exists
slice() Take() / Skip() Get portion of collection

Async/Await: C#’s async model is similar to JavaScript’s but with better performance characteristics and more control.

What’s Next?

As you begin this journey from JavaScript/TypeScript to C#, remember that your existing programming knowledge is valuable. The logical thinking, problem-solving skills, and understanding of programming concepts you’ve developed will serve you well. The main differences are in syntax, type system, and runtime environment – the core programming principles remain the same.

Focus on understanding C#’s type system first, then gradually explore its object-oriented features, and finally going into the rich .NET ecosystem. Your web development background gives you a solid foundation for understanding modern C# development, especially with technologies like ASP.NET Core for web APIs and Blazor for web applications.

For a structured learning path, check out Microsoft’s official roadmap for JavaScript and TypeScript developers learning C#, which provides additional guidance and examples to complement this guide.

Welcome to the C# community – you’re going to love the journey!