How to ensure your expert C# knowledge doesn’t make you a TypeScript noob

Coming from C# can quietly sabotage your TypeScript code in ways that are easy to overlook. Because the two languages share familiar syntax and patterns, it is natural to assume that the same instincts will carry over when modeling data or structuring state in an Angular app. But TypeScript behaves very differently once the types disappear at runtime, and approaches that make perfect sense in C# (like nullable flags, enums for everything, and class-driven thinking) can introduce unnecessary friction. Understanding where these habits diverge sets the stage for writing cleaner, more predictable TypeScript.

For full-stack developers, mental context switching never ends. When you’re moving between APIs and Angular apps, similarities across languages can feel deceptively comforting. Sometimes, this works in your favor. Concepts like ReactiveX and Observability translate cleanly between JavaScript and C#, so reusing mental models feels natural.

But other times, things only look similar on the surface. Looks like a duck, quacks like a duck, but it’s actually an excavator.

Bulldozer With Speech Bubble Reading Quack

The origin story

Many Microsoft-stack developers arrived at TypeScript accidentally. For years, Microsoft’s default web tooling centered on MVC, where the frontend was mostly static pages sprinkled with jQuery for interactivity. As those apps grew more complex, Microsoft needed a clearer frontend path, so they started including Angular templates out of the box.

Suddenly, C# developers found themselves working in Angular because it shipped with their project. TypeScript was not something we chose to learn. It was something we inherited.

This created a predictable misalignment. Developers treated their Angular code as an extension of their C# projects rather than a fundamentally different environment. And since C# is a strongly typed, runtime-aware language with features like reflection, many assumptions did not translate. TypeScript, by contrast, evaporates its types at build time. Your shipped code is plain JavaScript. No runtime types. No metadata. No safety net.

Yet TypeScript still has to support the entire expressive surface of JavaScript. That is why you can rename a .js file to .ts and watch everything still run. TypeScript’s type system must co-exist with JavaScript’s flexibility, which leads to some mind-bending patterns when you first encounter them.

This means one thing: approaching TypeScript with C# instincts can make your apps rigid, confusing, or unnecessarily defensive. I wrote TypeScript like a C# developer for years. It held me back. And if you learned C# first, the same might be happening to you.

It’s all about the types

Not “types” in the philosophical sense. The type keyword.

Many developers start with interfaces for everything. It is what the documentation shows. It is familiar. It feels class-adjacent.

Let’s imagine a simple API response shape:

interface ApiResponse {
  data: TypedResponseObjectDefinition,
  success?: boolean,
  errorMessage?: string,
  loading: boolean
}

interface TypedResponseObjectDefinition {
  id: number;
  name: string;
}

You know the drill. Infer loading, success, and error states based on combinations of nullable fields. Then wire up your fetch:

fetchData(): void {
  this.userId++;
  if (this.userId > 12) {
    this.userId = 0;
  }

  this.response$ = this.http.get<TypedResponseObjectDefinition>(
    'https://jsonplaceholder.typicode.com/users/' + this.userId
  ).pipe(
    map((x) => ({
      data: x,
      success: true,
      loading: false
    }) as ApiResponse),
    catchError((error: Error) => of<ApiResponse>({
      success: false,
      errorMessage: error.message,
      loading: false
    })),
    startWith({ loading: true } as ApiResponse)
  );
}

The component template is equally predictable. Check for loading. Then data. Otherwise errorMessage.

The problem is that this structure is incredibly fuzzy. You are relying on nullable properties and human intuition to describe state. It works, but it is brittle. It silently allows impossible states, for example an object with success: false and data populated.

Enter discriminated unions.

How type helps

Our API can only return three logical outcomes:

  • Success
  • Failure
  • Loading

We can express this directly using a union:

type ApiOutcome =
  | { state: 'SUCCESS'; data: TypedResponseObjectDefinition }
  | { state: 'ERROR'; error: string }
  | { state: 'LOADING' };

This unlocks one of TypeScript’s nicest features: exhaustive checking. Your IDE will generate all required branches inside a switch. Access is constrained to what each state actually allows. No nullable gymnastics. No speculative fields.

Updated fetch:

fetchData(): void {
  this.userId++;
  if (this.userId > 12) {
    this.userId = 0;
  }

  this.response$ = this.http.get<TypedResponseObjectDefinition>(
    'https://jsonplaceholder.typicode.com/users/' + this.userId
  ).pipe(
    map((x) => ({
      state: 'SUCCESS',
      data: x
    }) as ApiOutcome),
    catchError((error: Error) => of<ApiOutcome>({
      state: 'ERROR',
      error: error.message
    })),
    startWith({ state: 'LOADING' } as ApiOutcome)
  );
}

The template becomes drastically clearer:

@if (response$ | async; as response) {
  @switch (response.state) {
    @case ('LOADING') {
      Loading...
    }
    @case ('SUCCESS') {
      Employees name is {{ response.data.name }}
    }
    @case ('ERROR') {
      Error: {{ response.error }}
    }
  }
  <pre style="background-color: lightgray">{{ response | json }}</pre>
}

This version is expressive, impossible to misuse, and nearly self-documenting. Once you get used to it, it is hard to return to nullable booleans holding everything together with duct tape.

Sad, sad enums

C# developers love enums. They are safe, predictable, and clear. But TypeScript enums behave differently.

Consider:

enum ToddlersWords {
  Panday,
  Bridge,
  Bird,
  Spot,
  Flowers
}

String labeling with numeric backing. Sounds fine. Then you do this:

Object.keys(ToddlersWords);
Object.values(ToddlersWords);

You get both numbers and labels. Equality checks compare numeric representations, not strings. It is technically correct behavior, but rarely what you intend for UI logic or domain modeling.

Union time

If what you want is a set of string options, literal unions are the better tool:

const Words = ['Panday', 'Bridge', 'Bird', 'Spot', 'Flowers'] as const;
type ToddlerWords = typeof Words[number];

This keeps your domain values string-only, type-safe, and iterable without the strange enum duplication.

Defining arrays is straightforward:

toddlerWords: ToddlerWords[] = ['Panday', 'Bridge', 'Bird', 'Spot', 'Flowers'];

If you try to add a value that does not belong, TypeScript will complain at compile time instead of letting a bad state leak into your UI.

Harness the power of TypeScript

Writing clear, intentional TypeScript leads to simpler code and easier maintenance. The language has quirks, and its type system can feel alien when you are coming from a runtime-typed world like C#. But once you adopt patterns that align with JavaScript’s flexibility rather than resisting it, your apps get sturdier and your mental load shrinks.

If you want to explore the examples, feel free to clone the repo. And if your C# instincts shaped how you approached TypeScript, share how it went for you in the comments.

The post How to ensure your expert C# knowledge doesn’t make you a TypeScript noob appeared first on LogRocket Blog.

 

This post first appeared on Read More