The Convergence of C# and Typescript

By Tim Porter

Over the past few years, one of my roles has been to guide our .NET developers as we transition more of our applications to use modern front-end technologies like React and Typescript. When explaining a new concept or syntax I often find myself using existing C# examples to help developers make the connection between the two languages. An example I frequently use is the similarity between LINQ operators and Javascript array functions. For example, C#’s IEnumerable.Select() function and Javascript’s Array.map() operator.

// C#
var list = new List<int> {1, 2, 3};
var newList = list.Select((item) => item.ToString());
// Typescript
var list: Array<number> = [1, 2, 3];
var newList = list.map((item) => item.toString());

As you can see, the syntax between the two languages is strikingly similar. Over time a pattern has emerged as newer versions of C# and Typescript are released. C# is becoming more flexible, while Typescript is becoming more concrete. I’m sure it’s no coincidence that both of these languages are developed and maintained by Microsoft.

The first time I remember noticing the similarity was when C# introduced implicitly typed variables via the var keyword. This change allowed developers to worry less about defining every type in a way Javascript developers were already familiar with. This was a huge change that was met with hesitation by many developers who were used to being very explicit about types. And while Javascript has always been known as the “anything can be anything” language, Typescript has done an excellent job at protecting developers from themselves by mimicking the type inference patterns from other strongly-typed languages such as C#.

C#’s deconstruct syntax provides virtually the same functionality as the Javascript equivalents when operating on records or tuples.

// C#
record Vehicle(string Make, string Model);
var car = new Vehicle("Honda", "Civic");
var (make, model) = car;
// Typescript
type Vehicle = { make: string; model: string };
var car = { make: "Honda", model: "Civic" };
var { make, model } = car;

Typescript introduces OOP concepts such as interfaces and abstract classes on top of Javascript classes.

// Typescript
interface IPerson {
  firstName: string;
  lastName: string;
}

abstract class Person implements IPerson {
  firstName: string;
  lastName: string;

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

class Doctor extends Person {
  title: string;

  constructor(firstName: string, lastName: string, title: string) {
    super(firstName, lastName);
    this.title = title;
  }
}
// C#
interface IPerson {
  string FirstName;
  string LastName;
}

abstract class Person: IPerson {
  public string FirstName;
  public string LastName;

  public Person(string firstName, string lastName) {
    this.FirstName = firstName;
    this.LastName = lastName;
  }
}

class Doctor : Person {
  public string Title;

  public Doctor(string firstName, string lastName, string title): base(firstName, lastName) {
    this.Title = title;
  }
}

Typescript also adds support for generic type arguments in the same way C# does.

// Typescript
function getSize<T>(list: Array<T>): number {
  return list.length;
}
// C#
int getSize<T>(T[] list) {
  return list.Length;
}

C# supports anonymous types to encapsulate read-only data without the need to define the type beforehand and follows a similar syntax to object literals in Javascript.

// C#
var player = { name = "PacMan", score = 100 };
Console.WriteLine($"{player.name}: {player.score}");
// Typescript
const player = { name: "PacMan", score: 100 };
console.log(`${player.name}: ${player.score}`);

Notice how even the string interpolation syntax is similar!

Another syntax that has converged over time is async/await. Before the async/await syntax was introduced, developers commonly used either callbacks or Promises (or both) to execute asynchronous code and handle the responses. However, more complex asynchronous operations would quickly devolve into what is now referred to as “callback hell”. In C# this was complicated further by the use of delegates which involved many of the same pitfalls as the Javascript equivalent. In both languages these patterns were eventually replaced with a more succinct async/await syntax that operates similarly in both languages.

// Typescript
async function getItemsAsync(): Item[] {
  return await api.fetchItems();
}
// C#
async Item[] GetItemsAsync() {
  return await Api.FetchItems();
}

With the introduction of .NET 6 minimal APIs, even the structure of an application becomes harder to differentiate. Minimal APIs are a way to create a simple web API in .NET by abstracting away the boilerplate code that is typically needed to set up an application. This pattern seems almost directly inspired by the simple single-file nature of Node.js applications.

Here is Microsoft’s example of a minimal Web API:

// C#
var app = WebApplication.Create(args);
var port = "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");

And here is a similar example in Typescript using Express.js:

// Typescript
const app = require("express")();
const port = 3000;

app.get("/", (req, res) => res.send("Hello World!"));

app.listen(port);

Even the build tools are starting to look the same! The dotnet CLI tool follows many of the same patterns established by the Javascript ecosystem’s popular npm tool.

// installing packages
dotnet add package <name>
npm install <name>

// building an application
dotnet build
npm run build

// running an application
dotnet run
npm start

As these languages become more and more similar to one another, some may ask “when will one replace the other?” While front-end frameworks for C# such as Blazor do exist, and Node.js allows you to create applications written in Javascript on the server, I think both languages still have their place in the technology stack. However, in my opinion the real value comes from allowing developers to move between the two languages without having to remember a completely new set of syntax each time. I’m sure we will see more similarities emerge over time as each of these languages evolves to fit the needs of developers.