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:
- Performance is critical (e.g., real-time communication, video games)
- Need to interact with native code (e.g., C/C++ or Rust)
- 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.

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:
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
IntPtrto native objects - Using
GetFunctionPointerForDelegate()to passC#methods as function pointers to native functions - Using
Marshal.PtrToStringAnsi()to convertCstrings toC#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
- Performance:
C#is fast.C#'sASP.NET (Core) Webframework consistentlyranks highlyinTechempowerbenchmarks, and the performance of theC#.NET CoreCLR runtimeimproves with each major release. One reason forC#'s excellent performance is that by using structs instead of classes, applicationscan minimize or even eliminate garbage collection. Therefore,C#is very popular invideo game programming. - Games and mixed reality:
C#is one of the most popular languages for game development, with game engines likeUnity,Godot, and evenUnrealusingC#.C#is also popular in mixed reality becauseVRandARapplications are often written withUnity. - Because
C#has first-party libraries, tools, and documentation, some tasks are easier to achieve inC#, for example, creating agRPC clientis much more convenient inC#than inTypeScript. Conversely, when usingTypeScriptwithNode.js, you have to figure out the right combination of modules and tools to properly generate aJavaScript gRPC clientalong with correspondingTypeScripttypes. - Advanced features:
C#has many features not found in other languages, such asoperator 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 useRustfor its memory safety, but much of my work involves interfacing with existingC++code, so usingC++is easier.
Reference link: https://nate.org/csharp-and-typescript