Migrating from C# to TypeScript: mental model shifts
I've been writing C# professionally for about a year now on the backend of some ad serving systems, while also writing TypeScript on the frontend. Switching between the two throughout the day is interesting, and honestly it's made me a better developer in both languages.
Here are the mental model shifts that tripped me up the most.
Nullability is different
In C# (post nullable reference types in .NET 6+), nullability is explicit. string? is a nullable string, string is not. The compiler enforces this.
TypeScript's strictNullChecks does something similar, but the ecosystem assumes you're using it. If you come from older C# habits where nulls are everywhere and ignored, TypeScript strict mode will smack you hard.
My habit of writing return null for "nothing found" cases worked in C# with nullable types. In TypeScript I've moved toward returning undefined for optional values, or using a proper Option type for cases where I need to be explicit about absence.
Async is everywhere
In C# you have async/await but it's somewhat opt-in, and you still see a lot of synchronous code in service layers. In Node.js TypeScript, everything is async by default because the I/O model demands it.
The weirdest adjustment was unlearning the C# pattern of calling .Result on a Task<T> when you know it's already resolved. That blocks the thread. In Node.js, that pattern just doesn't exist — you await everything and the event loop handles the rest.
Type system expressiveness
C# generics are more constrained than TypeScript's type system. In TypeScript, you can do things like:
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
This kind of recursive mapped type is hard to replicate in C# generics. On the other hand, C# generics are more performant at runtime due to monomorphization, which doesn't apply to TypeScript (it all erases to JS anyway).
I now appreciate TypeScript's type system more for the kind of data transformation work I do on the frontend, and C#'s type system more for the strict OOP contracts in backend service layers.
The tooling gap is real but closing
C#'s tooling in Rider/Visual Studio is still significantly ahead of TypeScript for refactoring. Rename across solution, find all references, extract interface — all work flawlessly in C#. In TypeScript it's gotten a lot better but there are still edge cases.
That said, TypeScript's iteration speed beats C# by a mile. No compile step, hot reloading, instant feedback. For frontend work that tradeoff is absolutely worth it.
What I took back to TypeScript
Working in C# made me more disciplined about interfaces and contracts. I started defining explicit interfaces for all my TypeScript service classes even when it felt like overkill. Turns out it makes mocking for tests significantly cleaner.
interface AdService {
fetchAd(params: AdRequestParams): Promise<AdResponse>;
recordImpression(adId: string): Promise<void>;
}
If you're switching between the two languages, lean into the differences rather than fighting them. Each has idioms that make sense in context.