Is C# the Best Substitute for TypeScript?

Is C# the Best Substitute for TypeScript?

TypeScript is excellent. It perfectly combines strong typing and rapid development, making it very useful. In many cases, I default to this library. However, no language is perfect; in some situations, TypeScript is not the most suitable tool:

Last updated 12/27/2021 8:53 PM
CSDN
14 min read
Category
.NET
Tags
.NET C# TypeScript

Author | Nate Hill

Translator | Wanyue

Source | CSDN (ID: CSDNnews)

TypeScript is excellent. It perfectly combines strong typing with rapid development, making it incredibly useful, and I default to it in many situations. However, no language is perfect, and there are cases where TypeScript is not the most suitable tool:

  1. Performance is critical (e.g., real-time communication, video games)
  2. Need to interact with native code (e.g., C/C++ or Rust)
  3. Need a stricter type system (e.g., financial systems)

In these cases, TypeScript developers would be better off choosing another language. C#, Go, and Java are all excellent choices. They are significantly faster than TypeScript, and each has its own strengths. C# works particularly well with TypeScript, and let me explain why.

Source: CSDN paid download from Oriental IC

1. TypeScript is JavaScript with C# added

C# works well with TypeScript because they look like the same language. Both were designed by Anders Hejlsberg, and in many respects, TypeScript is JavaScript with C# added. Their features and syntax are very similar, making it easy to combine the two in the same project. More importantly, C#'s language is very similar to TypeScript, so developers find it easy to read and write code in both.

In contrast, Go is a completely different language: no classes, no inheritance, no exceptions, no package-level encapsulation (only class-level encapsulation), and completely different syntax. That's not necessarily bad, but it does require developers to rethink and design code differently, making it more difficult to use Go and TypeScript together. Java is similar to C#, but still lacks many features that C# and TypeScript both have.

2. Similarities between C# and TypeScript

You may already know that C# and TypeScript share many similarities, such as C-based syntax, classes, interfaces, generics, etc. Let me detail their similarities:

  • 2.1 async/await
  • 2.2 Lambda expressions and functional array methods
  • 2.3 Null-handling operators (?, !, ??)
  • 2.4 Destructuring
  • 2.5 Command-line interface (CLI)
  • 2.6 Basic features (classes, generics, errors, and enums)

2.1 async/await

First, both C# and JavaScript use async/await for asynchronous code. In JavaScript, asynchronous operations are represented by Promises, and applications can await the completion of an async operation. In C#, the equivalent of a Promise is a Task, which is conceptually identical and has corresponding methods. The following example demonstrates async/await usage in both languages:

Example of async/await in TypeScript:

async function fetchAndWriteToFile(url: string, filePath: string): Promise<string> {
  // fetch() returns a Promise
  const response = await fetch(url);
  const text = await response.text();
  // By the way, we're using Deno (https://deno.land)
  await Deno.writeTextFile(filePath, text);
  return text;
}

Example of async/await in C#:

using System.IO;
using System.Net.Http;
using System.Threading.Tasks;

async Task<string> FetchAndWriteToFile(string url, string filePath) {
  // HttpClient.GetAsync() returns a Task
  var response = await new HttpClient().GetAsync(url);
  var text = await response.Content.ReadAsStringAsync();
  await File.WriteAllTextAsync(filePath, text);
  return text;
}

Here are the JavaScript Promise API and their equivalent C# Task APIs:

JavaScript API Equivalent C# API
Promise.all() Task.WaitAll()
Promise.resolve() Task.FromResult()
Promise.reject() Task.FromException()
Promise.prototype.then() Task.ContinueWith()
new Promise() new TaskCompletionSource()

2.2 Lambda expressions and functional array methods

Both C# and JavaScript use the familiar => syntax (i.e., arrow functions) to represent lambda expressions. Below is a comparison of TypeScript and C#:

Using lambda expressions in TypeScript:

const months = ['January', 'February', 'March', 'April'];
const shortMonthNames = months.filter(month => month.length < 6);
const monthAbbreviations = months.map(month => month.substr(0, 3));
const monthStartingWithF = months.find(month => {
  return month.startsWith('F');
});

Using lambda expressions in C#:

using System.Collections.Generic;
using System.Linq;

var months = new List<string> {"January", "February", "March", "April"};
var shortMonthNames = months.Where(month => month.Length < 6);
var monthAbbreviations = months.Select(month => month.Substring(0, 3));
var monthStartingWithF = months.Find(month => {
  return month.StartsWith("F");
});

The above examples demonstrate some methods from C#'s System.Linq namespace, which correspond to JavaScript's functional array methods. Below are JavaScript's array methods and their equivalent C# Linq methods:

JavaScript API Equivalent C# API
Array.prototype.filter() Enumerable.Where()
Array.prototype.map() Enumerable.Select()
Array.prototype.reduce() Enumerable.Aggregate()
Array.prototype.every() Enumerable.All()
Array.prototype.find() List.Find()
Array.prototype.findIndex() List.FindIndex()

2.3 Null-handling operators

C# and TypeScript share the same features for handling null:

Feature name Syntax Documentation links
Optional properties property? TS: https://www.typescriptlang.org/docs/handbook/2/objects.html#optional-properties,
C#: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-reference-types
Non-null assertion object!.property TS: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator,
C#: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-forgiving
Optional chaining object?.property JS: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining,
C#: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-
Nullish coalescing object ?? alternativeValue JS: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator,
C#: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator

2.4 Destructuring

Although C# does not natively support destructuring of arrays or classes, it supports destructuring of Tuples and Records, and users can define destructuring for custom types. Below are examples of destructuring in TypeScript and C#:

Example of destructuring in TypeScript:

const author = { firstName: 'Kurt', lastName: 'Vonnegut' };
// Destructuring an object:
const { firstName, lastName } = author;

const cityAndCountry = ['Indianapolis', 'United States'];
// Destructuring an array:
const [city, country] = cityAndCountry;

Example of destructuring in C#:

using System;

var author = new Author("Kurt", "Vonnegut");
// Deconstructing a record:
var (firstName, lastName) = author;

var cityAndCountry = Tuple.Create("Indianapolis", "United States");
// Deconstructing a tuple:
var (city, country) = cityAndCountry;

// Define the Author record used above
record Author(string FirstName, string LastName);

2.5 Command-line interface (CLI)

My development style is to write code in a text editor and then run commands in the terminal to build and run. For TypeScript, this means using the node or deno CLI. C# also has a similar CLI called dotnet (named after C#'s .NET runtime). Here are some examples of using the dotnet CLI:

mkdir app && cd app
# Create a new console application
# List of available app templates: https://docs.microsoft.com/dotnet/core/tools/dotnet-new
dotnet new console
# Run the app
dotnet run
# Run tests (don't feel bad if you haven't written those)
dotnet test
# Build the app as a self-contained
# single file application for Linux.
dotnet publish -c Release -r linux-x64

2.6 Basic features (classes, generics, errors, and enums)

These are more fundamental similarities between TypeScript and C#. The following examples demonstrate these aspects:

Example of a TypeScript class:

import { v4 as uuidv4 } from 'https://deno.land/std/uuid/mod.ts';

enum AccountType {
  Trial,
  Basic,
  Pro
}

interface Account {
  id: string;
  type: AccountType;
  name: string;
}

interface Database<T> {
  insert(item: T): Promise<void>;
  get(id: string): Promise<T>;
}

class AccountManager {

  constructor(database: Database<Account>) {
    this._database = database;
  }

  async createAccount(type: AccountType, name: string) {
    try {
      const account = {
        id: uuidv4(),
        type,
        name
      };
      await this._database.insert(account);
    } catch (error) {
        console.error(`An unexpected error occurred while creating an account. Name: ${name}, Error: ${error}`);
    }
  }

  private _database: Database<Account>;
}

Example of a C# class:

using System;
using System.Threading.Tasks;

enum AccountType {
  Trial,
  Basic,
  Pro
}

record Account(string Id, AccountType Type, string Name);

interface IDatabase<T> {
  Task Insert(T item);
  Task<T> Get(string id);
}

class AccountManager {

  public AccountManager(IDatabase<Account> database) {
    _database = database;
  }

  public async void CreateAccount(AccountType type, string name) {
    try {
      var account = new Account(
       Guid.NewGuid().ToString(),
        type,
        name
      );
      await _database.Insert(account);
    } catch (Exception exception) {
     Console.WriteLine($"An unexpected error occurred while creating an account. Name: {name}, Exception: {exception}");
    }
  }

  IDatabase<Account> _database;
}

3. Additional advantages of C#

Similarity to TypeScript is not the only advantage of C#; it has other benefits:

  • 3.1 Easier integration with native code
  • 3.2 Events
  • 3.3 Other features

3.1 Integration with native code

One of the biggest advantages of C# is its ability to go deep into native code. As mentioned at the beginning, TypeScript is not good at combining with C/C++ code. Node.js has a plugin for native C/C++ called Node-API, but it requires writing additional C++ wrappers for native functions to convert native types to JavaScript objects and vice versa, similar to JNI. In contrast, C# can directly call native functions by simply placing the library in the application's bin directory and defining the API as external functions in C#. You can then use native functions just like C# functions, and the .NET runtime handles the conversion between C# data types and native data types. For example, if a native library exports the following C function:

int countOccurrencesOfCharacter(char *string, char character) {
    int count = 0;
    for (int i = 0; string[i] != '\0'; i++) {
        if (string[i] == character) {
            count++;
        }
    }
    return count;
}

Then it can be called from C# like this:

using System;
using System.Runtime.InteropServices;

var count = MyLib.countOccurrencesOfCharacter("C# is pretty neat, eh?", 'e');
// Prints "3"
Console.WriteLine(count);

class MyLib {
    // Just place MyLibraryName.so in the app's bin folder
   [DllImport("MyLibraryName")]
    public static extern int countOccurrencesOfCharacter(string str, char character);
}

This method allows access to any dynamic library (.so, .dll, or .dylib) through C linking, meaning you can easily call code written in C, C++, Rust, Go, or other languages as long as they are compiled to machine code. Other applications of native interop include:

  • Passing pointers as IntPtr to native objects
  • Using GetFunctionPointerForDelegate() to pass C# methods as function pointers to native functions
  • Using Marshal.PtrToStringAnsi() to convert C strings to C# strings
  • Converting structures and arrays

3.2 Events

A unique feature of C# is its first-class support for events. In TypeScript, you can implement the addEventListener() method to allow clients to listen to events, but C# has the event keyword, which can be used to define events and notify all listeners with simple syntax (without needing to manually iterate over all event listeners and execute them in try/catch blocks like in TypeScript). For example, we can define a MessageReceived event in a Connection class as follows:

class Connection {
    // An Action<string> is a callback that accepts a string parameter.
    public event Action<string> MessageReceived;
}

Code using Connection can add a handler to MessageReceived using the += operator, like this:

var connection = new Connection();
connection.MessageReceived += (message) => {
   Console.WriteLine("Message was received: " + message);
};

The Connection class can internally invoke MessageReceived to trigger the MessageReceived event for all listeners:

// Raise the MessageReceived event
MessageReceived?.Invoke(message);

4. Other advantages

  1. Performance: C# is fast. C#'s ASP.NET (Core) Web framework consistently ranks highly in Techempower benchmarks, and the performance of the C# .NET CoreCLR runtime improves with each major release. One reason for C#'s excellent performance is that by using structs instead of classes, applications can minimize or even eliminate garbage collection. Therefore, C# is very popular in video game programming.
  2. Games and mixed reality: C# is one of the most popular languages for game development, with game engines like Unity, Godot, and even Unreal using C#. C# is also popular in mixed reality because VR and AR applications are often written with Unity.
  3. Because C# has first-party libraries, tools, and documentation, some tasks are easier to achieve in C#, for example, creating a gRPC client is much more convenient in C# than in TypeScript. Conversely, when using TypeScript with Node.js, you have to figure out the right combination of modules and tools to properly generate a JavaScript gRPC client along with corresponding TypeScript types.
  4. Advanced features: C# has many features not found in other languages, such as operator overloading, destructors, etc.

5. Summary

As mentioned earlier, no language is perfect. There are always trade-offs when designing a language, so some languages are faster but more difficult to use (e.g., Rust's borrow checker). On the other hand, some languages are very easy to use, but optimizing performance is often more difficult (e.g., JavaScript's dynamic language features). For this reason, I believe it is very useful to master a set of similar languages: each has its own strengths, but they are similar and can work together. For example, here is the set of languages I choose:

5.1 TypeScript

  • The highest-level language with the fastest development speed
  • Performance is not optimal, but sufficient for most applications
  • Not well suited for integrating with native code

5.2 C#

  • Still a high-level language with garbage collection, so it's easy to use, though not as easy as TypeScript
  • Performance in terms of speed and memory footprint is better than TypeScript
  • Most importantly, it can interface well with low-level code

5.3 C++

  • More difficult to develop (e.g., manual memory management), so development speed is much slower
  • But the best runtime performance! Also widely available and can interface with many existing software
  • Similar to C# and has a good standard library, but also many pitfalls (mostly related to memory management). I would prefer to use Rust for its memory safety, but much of my work involves interfacing with existing C++ code, so using C++ is easier.

Reference link: https://nate.org/csharp-and-typescript

Keep Exploring

Related Reading

More Articles