DEV Community

Cover image for 3 TypeScript Features That Make C# Developers Jealous
Daniel Rusnok
Daniel Rusnok

Posted on • Originally published at levelup.gitconnected.com

3 TypeScript Features That Make C# Developers Jealous

If you're coming from C#, these three TypeScript features will change how you think about types.

Three years ago I was writing CQRS handlers in .NET. Today I'm building React components. TypeScript felt familiar — and then it felt weird.

My first weeks on the frontend were humbling. A senior developer told me to stop using let and always use const — something about immutability that felt obvious to him and completely foreign to me. I was also regularly reminded that we had TypeScript and I kept writing plain JavaScript anyway. Old habits.

But once I stopped fighting the ecosystem and started learning its rules, I found features I genuinely wish C# had. Here are three of them.


Union Types with Literal Values

In C#, when you need a property with a fixed set of string values, you reach for an enum. It works — but a string and an enum are two different things, and you're always mapping between them.

// Doesn't work — string and enum are different types
if (request.Status == OrderStatus.Pending)

// You need explicit conversion
if (Enum.Parse<OrderStatus>(request.Status) == OrderStatus.Pending)

// Modern .NET helps with automatic conversion via attribute,
// but you still define the enum separately and add the plumbing:
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum OrderStatus { Pending, Processing, Completed }
Enter fullscreen mode Exit fullscreen mode

In TypeScript, a string literal is the type. No conversion, no attribute:

type OrderStatus = 'pending' | 'processing' | 'completed';
if (status === 'pending') { ... }
Enter fullscreen mode Exit fullscreen mode

I first reached for this with a Button component variant prop — 'primary' | 'secondary'. No separate file, no mapping. The type is the documentation, and the compiler enforces it immediately.


Structural Typing

In C#, a type is what it declares itself to be. If a method expects IAnimal, the object must explicitly implement IAnimal. The compiler checks names, not "shapes".

TypeScript works the opposite way. If your object has the right properties, it is the right type — whether it says so or not:

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

function greet(user: User) {
  console.log(`Hello, ${user.name}`);
}

const person = { id: 1, name: 'Daniel', age: 30 };
greet(person);
Enter fullscreen mode Exit fullscreen mode

My first instinct was to fight it. I added as User everywhere, or used satisfies User just so I could see the type and navigate the codebase by type references. Later, I got used to it and stopped adding types explicitly. The IDE watches type mismatches for me in real time.


Utility Types

C# developers are used to writing new types when they need a variation of an existing one. A DTO with fewer fields? New class. An update model with all optional fields? New class. A read-only projection? New class.

TypeScript ships with a set of utility types that transform existing types instead:

interface ComponentProps {
  id: number;
  title: string;
  description: string;
  onSave: () => void;
  onDelete: () => void;
}

// Sub-component only needs part of the props
type HeaderProps = Pick<ComponentProps, 'id' | 'title'>;

// Update form - everything optional
type UpdateProps = Partial<ComponentProps>;

// Remove internal handlers from public API
type PublicProps = Omit<ComponentProps, 'onDelete'>;
Enter fullscreen mode Exit fullscreen mode

My favorite is Pick, and I use it exactly like this in production. When a parent component has a large props interface, sub-components shouldn't receive everything — they work with a slice. Pick lets you define that slice without creating a new type from scratch or reaching for inheritance.

In C#, you'd either duplicate properties in a new class, use inheritance (which often brings unwanted baggage), or reach for libraries like AutoMapper to handle projections. TypeScript just gives you the tools.


Bonus — "as const"

In C#, you'd use readonly or const to prevent mutation. TypeScript has something more powerful: as const freezes not just the value, but the type itself.

Without as const:

const directions = ['north', 'south', 'east', 'west'];
// TypeScript infers: string[]
Enter fullscreen mode Exit fullscreen mode

With as const:

const directions = ['north', 'south', 'east', 'west'] as const;
// TypeScript infers: readonly ['north', 'south', 'east', 'west']
Enter fullscreen mode Exit fullscreen mode

Now directions isn't just an array of strings — it's a tuple of exactly those four literal values. You can even derive a type from it:

type Direction = typeof directions[number];
// 'north' | 'south' | 'east' | 'west'
Enter fullscreen mode Exit fullscreen mode

No separate type definition. No enum. The array is the source of truth, and the type follows automatically.


Conclusion

Union types, structural typing, and utility types aren't just syntax sugar. They represent a different philosophy: types should describe your data, not govern your class hierarchy.

Coming from C#, that shift takes time. But once it clicks, you start wishing the CLR worked the same way.

If you're a .NET developer exploring TypeScript, what feature surprised you most? Let me know in the comments.

Top comments (0)