From JavaScript/TypeScript to C#: A Developer's Migration Guide
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
- Same Syntax Pattern: Both use the
=>
operator (fat arrow) - Expression vs Statement Forms: Both support single expressions and multi-statement blocks
- Type Inference: Both can infer types in many contexts
- Higher-Order Functions: Both can be passed as parameters and returned from functions
- 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.
Reference Links and Further Reading
- Microsoft Learn - C# documentation
- Tips for JavaScript Developers Learning C# - Microsoft’s official roadmap
- .NET Platform Overview
- ASP.NET Core documentation
- Blazor documentation
- Lambda expressions in C#
- Arrow functions
- Language Integrated Query (LINQ)
- Node.js TypeScript Support
- Anonymous Methods in C#
- C# Pattern Matching
- Record Types in C#
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!