Skip to main content

Command Palette

Search for a command to run...

Is TypeScript More Technical Than .NET? The Question Behind the Question

Updated
•8 min read•View as Markdown
Is TypeScript More Technical Than .NET? The Question Behind the Question

TypeScript was led by Anders Hejlsberg, the same engineer who is the lead architect of C#. So when someone asks me whether TypeScript is “more technical” than .NET, I find it a little funny. Both stacks come from the same thinking about what a type system should do for you.

I get this question from developers choosing a career path, from people comparing job listings, and sometimes from engineers who have already picked a side and want backup. I’ve built production systems with both, and the honest answer is that the question is slightly broken. Here’s why, and what a better question looks like.

They aren’t the same kind of thing

TypeScript is a language. It’s a typed superset of JavaScript that compiles down to plain JavaScript.

.NET is a platform: a runtime (the CLR), compilers, a base class library, SDK tooling, and frameworks like ASP.NET Core and EF Core.

So the fair comparisons are:

TypeScript with C#, for languages Node.js (or Deno, or Bun) with the .NET runtime, for platforms

Most of the confusion in this debate comes from comparing a language on one side to an entire platform on the other.

Where TypeScript goes deep: a type system you can program

TypeScript’s type system is structural. It cares about the shape of a value, not the name of its type.

typescript interface Payable { amount: number; currency: string; }

const invoice = { amount: 500, currency: "NGN", dueDate: "2026-11-01" };

function collect(p: Payable) { // ... }

collect(invoice); // compiles, the shape fits

That alone is useful. The depth shows up when you start transforming types. Mapped types rebuild a type from another one:

typescript type Nullable = { [K in keyof T]: T[K] | null }; type PartialInvoice = Nullable;

Conditional types with infer let you extract types:

typescript type Unwrap = T extends Promise ? U : T;

type A = Unwrap; // string type B = Unwrap; // number

Template literal types build string types:

typescript type Changed = ${T}Changed; type BalanceEvent = Changed<"balance">; // "balanceChanged"

This is real programming, done at compile time. It’s how libraries infer route parameters, query results and form shapes from your own code. A developer who truly understands this layer is working at a genuinely high technical level.

The catch: the types disappear

TypeScript’s types are erased when the code compiles. What runs in production is JavaScript with no type information at all. The compiler trusts whatever you tell it:

typescript interface User { id: number; email: string; }

const user = (await response.json()) as User; // the compiler believes you

If the API returns something else, nothing stops it. Your types now describe a world that doesn’t exist, and the error shows up three functions later as undefined is not a function.

The fix is to validate at the boundary, with a schema library like Zod, and derive the type from the schema so the two can’t drift apart:

typescript import { z } from "zod";

const UserSchema = z.object({ id: z.number(), email: z.string(), });

type User = z.infer;

const user = UserSchema.parse(await response.json()); // throws if the data is wrong

Understanding exactly where TypeScript’s guarantees end is a big part of being good with it.

Where .NET goes deep: types the runtime actually enforces

C# is nominal. Two types with identical shapes are still different types, and the runtime enforces that, not just the compiler.

This matters more than it sounds. Take money:

csharp public record Naira(decimal Amount); public record Dollar(decimal Amount);

static void Charge(Naira price) { /* ... */ }

Charge(new Dollar(5m)); // compile error, different types

The TypeScript equivalent compiles happily, because the shapes match:

typescript type Naira = { amount: number }; type Dollar = { amount: number };

declare function charge(price: Naira): void; declare const usd: Dollar;

charge(usd); // compiles

TypeScript developers solve this with branded types:

typescript type Naira = number & { readonly __brand: "NGN" }; type Dollar = number & { readonly __brand: "USD" };

It works, but it’s a pattern you add on purpose, where C# gives it to you by default.

Reified generics

In C#, generics survive to runtime:

csharp Console.WriteLine(typeof(List)); // System.Collections.Generic.List1[System.Int32] Console.WriteLine(typeof(List<string>)); // System.Collections.Generic.List1[System.String]

These are two distinct types at runtime. A List stores ints directly, with no boxing, because the JIT generates specialized code for value types. In TypeScript, Array and Array are the same array once compiled.

A runtime you can reason about

JavaScript engines like V8 have sophisticated JIT compilers and garbage collectors too. The difference is how much of that surface a typical application developer is expected to understand and work with.

In .NET, that surface is part of the job. Tiered JIT compilation, Dynamic PGO (on by default since .NET 8), a generational GC, and a real thread pool all affect how your service behaves under load. And when profiling points at a hot path, you can drop to low-level code without leaving C#:

csharp using System.Globalization;

static decimal ParseAmount(ReadOnlySpan line) { var first = line.IndexOf('|'); var rest = line[(first + 1)..]; var second = rest.IndexOf('|');

return decimal.Parse(rest[..second], NumberStyles.Number, CultureInfo.InvariantCulture);

}

Given a line like NGN|150000|REF123, this returns 150000 without creating a single substring or array. The Split('|') version allocates a string[] plus a new string for every field.

Concurrency: two different mental models

JavaScript runs your code on one main thread with an event loop. That makes a lot of bugs impossible, but it creates its own failure mode: one CPU-heavy task blocks every other request. The answer is worker threads, which communicate by message passing by default instead of sharing memory.

.NET gives you real OS threads, a thread pool, and shared memory by default. That means true parallelism, and it also means data races, which is why lock, Interlocked and Channel exist. It also means failure modes like thread pool starvation from blocking on async code.

Neither model is easier. They fail differently. A small piece of history: async/await shipped in C# 5 in 2012, years before JavaScript adopted the same syntax in ES2017.

The ecosystem difficulty nobody counts

A lot of the real difficulty in TypeScript projects lives outside the language: ES modules versus CommonJS, tsconfig options, bundlers, and differences between Node, browser and edge runtimes. In my experience, TypeScript projects spend much of their complexity budget on tooling and module resolution, while .NET projects spend theirs on runtime behavior and architecture. Both are real, and they’re different kinds of hard.

So which one is more technical? TypeScript .NET Type system Structural, very expressive, erased at compile time Nominal, enforced at runtime Concurrency model Single-threaded event loop, worker threads Real threads, thread pool, shared memory Where the depth lives Type-level programming, tooling Runtime internals, memory, concurrency Typical failure mode Types that lie at runtime Data races, GC pauses, thread pool starvation

TypeScript is deeper at compile time. .NET is deeper at runtime. Neither is “more technical,” because they go deep in different places.

What actually makes work technical

The hardest problems I’ve worked on, on payments, insurance and lending platforms, never cared which language they were written in. Making a payment endpoint idempotent. Keeping data consistent across services. Staying inside a latency budget on a fraud check. Controlling who can see sensitive records, and why.

Those problems show up in both stacks, and they’re what separate senior engineers from everyone else. The language is just the material you work with.

What I’d do if you’re choosing

If you’re learning TypeScript, go deep on the type system, runtime validation at your boundaries, and the event loop. If you’re learning .NET, go deep on async, memory and the GC, EF Core query behavior, and then runtime internals.

Better still, learn both layers in both stacks. Plenty of teams pair a TypeScript frontend with a .NET backend, and knowing exactly where each side’s guarantees stop is what makes you useful at the seam between them.

The better question

Instead of asking which stack is more technical, ask: where does my stack’s guarantee end, and what am I doing about it?

In TypeScript, the guarantee ends at the network boundary, so you validate. In .NET, type safety doesn’t protect you from logic errors or concurrency bugs, so you design for them. The engineers who do well in either stack are the ones who know exactly where the safety net stops.

Which layer has taught you more, the type system or the runtime, and in which stack did you learn it?