How ToT Light_870x220

This post gets C# developers started building applications with TypeScript, which allows static typing combined with the enormous ecosystem of JavaScript.

If you ever worked on a JavaScript application in the old days and have never had a chance to do the same with modern tooling and ecosystem, you would be surprised by how both the development and the production experiences are different and immensely improved. This is especially the case if you are coming from a .NET and C# background. C# is a very productive, general purpose programming language. Today, you can build games, web, desktop and mobile applications with C# running on .NET Core, Mono and various other available runtimes. JavaScript mostly offers the same, but the biggest gap is missing the comfort and safety of static typing.

Static typing allows us to catch many typical programming errors and mistakes during compilation time instead of noticing these in runtime. When you combine this fact with great tooling and code editor support, you achieve a much faster feedback loop which then gives you a higher chance to build more robust applications in a much quicker way.

TypeScript gives you just that, and it does it without taking the best of JavaScript from you: the enormous ecosystem! All the packages available on npm, for instance, are available for you to tap into even if they are not implemented in TypeScript. This is huge, and it's possible due to one of the core principles of TypeScript: it starts and ends with JavaScript. We will shortly see an example of how this is actually made possible in practice.

The main aim of this post is to get you started on building applications with TypeScript and also to give you a sense of how this process looks from the development perspective. It's highly encouraged that you first have a look at “TypeScript in 5 Minutes” if you don't have prior experience with TypeScript and how it functions at a high level.

Code is Read More Than Written

My experience proves Uncle Bob's quote on the ratio of code reading versus writing being over 10-to-1. So, it's only fair to evaluate and see the power of TypeScript in terms of code reading aspect.

Let's clone one of the biggest open-source TypeScript projects: Visual Studio Code. What I am after here is to learn how we should be implementing a command in order to execute it through the Visual Studio Code command palette. A quick search on GitHub got me the blockCommentCommand.ts file, and it revealed a class which implemented an interface: editorCommon.ICommand. Let's get the tooling help from Visual Studio Code (meta!) and find out about the interface definition:

Visual Studio Code: Peek Definition

Visual Studio Code: Peek Definition Interface

We are directly inside the interface, and the most important part here is the explicitness, how apparent the extensibility point is. I want to understand this interface further though. So, let's look into the getEditOperations() function of the interface and find out all of its implementations:

Visual Studio Code: Find All References

Visual Studio Code: Find All References Results

This is really nice, and what's really good here is that it's not a string find. It's actually finding all the interface function implementations. sortLinesCommand seems interesting to me and I want to dive into it. Let's go into it and start changing it. The first thing I am noticing is the help I get from the code editor.

Visual Studio Code: public getEditOperations

You can see that all of this tooling help, which comes from the power of static analysis abilities of the language itself, makes it super easy for me to explore an unfamiliar and fairly large codebase. This is the core value proposition of TypeScript.

Now that we understand the main value of the language, let's look at some of its aspects by going through a sample.

A Sample Application

Let's look at a sample application to understand how we can structure the codebase with TypeScript. In this sample, we will see:

  • How to work with TypeScript primitives such as Classes
  • Consuming NPM packages implemented in pure JavaScript
  • Running the application on Node.js

The core logic of the application is around voting for particular topics that have some options. I have structured the code logic of this application inside the domain.ts file, which has the below content.

import { v4 as uuid } from 'uuid';

export class TopicOption {
  readonly name: string;
  readonly id: string;

  constructor(name: string) {
    this.name = name
    this.id = uuid()
  }
}

interface Winner {
  readonly option: TopicOption;
  readonly votes: number;
}

export class Topic {
  readonly name: string;
  readonly options: TopicOption[];
  private readonly _votes: Map<string, number>;

  constructor(name: string, options: TopicOption[]) {
    this.name = name
    this.options = options;
    this._votes = new Map<string, number>();
    options.forEach(option => {
      this._votes.set(option.id, 0);
    })
  }

  vote(optionId: string) {
    const votesCount = this._votes.get(optionId);
    if (votesCount != undefined) {
      this._votes.set(optionId, votesCount + 1);
    }
  }

  getWinner(): Winner {
    const winner = [...this._votes.entries()].sort((a, b) => a[1] > b[1] ? 1 : -1)[0];
    const option = this.options.find(x => x.id == winner[0]);

    if (option == undefined) {
      throw "option has no chance to be undefined here";
    }

    return {
      option: option,
      votes: winner[1]
    };
  }
}

If you have a C# background, this code should be mostly self-explanatory. However, it's worth specifically touching on some of its aspects to understand how TypeScript lets us structure our code.

Say “Hello” to Classes

The Class construct sits at the heart of the C# programming language and it's mostly how we model our applications and domain concepts. TypeScript has the same modeling concept, which is close to classes in ECMAScript 6 (ES6) but with some key additional “safety nets.”

Assume the following:

export class Topic {
  readonly name: string;
  readonly options: TopicOption[];
  private readonly _votes: Map<string, number>;

  constructor(name: string, options: TopicOption[]) {
    this.name = name
    this.options = options;
    this._votes = new Map<string, number>();

    options.forEach(option => {
      this._votes.set(option.id, 0);
    })
  }

  // ...
}

A few things to note here:

  • We are able to have readonly and private fields, just like we can in C#. That's big! Especially for cases where we start passing around an instance of a class, we want to be confident that it will stay intact and won't get any unintended modifications. private and readonly access modifiers allow us to express this intent in code and allow the compiler to protect against these unintended modifications.
  • We have the notion of a constructor as well. This is again a big win, allowing us to express what makes up a class instance in order to enforce a certain level of state integrity for the instance.

Structural Typing

The TypeScript type system is based on structural typing. This essentially means that it's possible to match the types based on their signatures. As we have seen with the getWinner() method implementation above, the signature requires us to return the Winner interface. However, we didn't need to create a class which explicitly implemented the Winner interface. Instead, it was enough for us to new up an object which matched the interface definition:

getWinner(): Winner {
  // ...

  return {
    option: option,
    votes: winner[1]
  };
}

This is really efficient when implementing logic, as you don't need to explicitly implement an interface. This aspect of the language also protects us in case of potential changes to the signature of the Winner interface. Let's take the following change to the signature of the Winner interface by adding a new property called margin.

interface Winner {
  readonly option: TopicOption;
  readonly votes: number;
  readonly margin: number;
}

As soon as making this change, Visual Studio Code highlights a problem:

Visual Studio Code: Property 'margin' is missing but required in type 'Winner'

No More Billion Dollar Mistakes: Protection Against null and undefined

Don't you love NullReferenceException when working with C#? I can hear you! Not so much, right? Well, you are not alone. In fact, null references have been identified as a billion dollar mistake. TypeScript helps us on this problem as well. By setting the compiler flag --strictNullChecks, TypeScript doesn't allow you to use null or undefined as values unless stated explicitly.

In our example of returning a winner from the getWinner() method, we would see an error if we were to assign null to the option property.

Visual Studio Code: error for assigning null to option

TypeScript provides the same level of compiler safety across the board for all relevant members. For example, we can see that we are unable to pass null or undefined as a parameter to a method which accepts a string:

Visual Studio Code: unable to pass null or undefined as a parameter to a method which accepts a string

However, there are sometimes legitimate cases for us to represent a value as null or undefined. In these circumstances, we have several ways to explicitly signal this. For example, one way is to mark the type as nullable by adding ? at the end of the member name:

interface Winner {
  readonly option?: TopicOption;
  readonly votes: number;
}

This allows us to pass undefined as a legitimate value to this or entirely omit the assignment. For example, both the below representations are correct to match the object to Winner interface:

return {
  option: undefined,
  votes: winner[1]
};

// ...

return {
  votes: winner[1]
};

TypeScript doesn't stop here. It also helps you when you are consuming a member which can be null or undefined. For example, we would see an error like below if we were to try to reach out to the members of the TopicOption value returned from the getWinner() method:

Visual Studio Code: object is possibly undefined

This behaviour of TypeScript forces us to guard against null and undefined. Once we perform the check, the compiler will then be happy for us to access the members of TopicOption:

Visual Studio Code: TopicOption id

Visual Studio Code: TopicOption name

Consuming a JavaScript npm Package

As you can see in the below code, we are able to import a package called uuid which we installed through npm install uuid --save.

import { v4 as uuid } from 'uuid';

This package is implemented in JavaScript but we are still able to consume it and get the tooling and type safety support as we get for pure TypeScript code. This is possible thanks to the notion of TypeScript declaration files. It's a very deep topic to get into here, but the best piece of advice I can give here is to check TypeScript type search, where you can find and install the declaration files.

typesearch

Executing on Node.js

TypeScript compiles down to JavaScript, and this means that we can run our applications anywhere we are able to execute JavaScript, as long as we compile our source code in a way that the executing platform knows how to interpret. This mostly means targeting the correct ECMAScript version through the –target compiler flag and specifying the relevant module code generation through the --module compiler flag.

Let's look at how we can compile this small voting logic to be executed on Node.js. The below is the content of index.ts:

import { Topic, TopicOption } from './domain';

const topics: Topic[] =[
  new Topic("Dogs or cats?", [
    new TopicOption("Dogs"),
    new TopicOption("Cats")
  ]),
  new Topic("C# or TypeScript?", [
    new TopicOption("C#"),
    new TopicOption("TypeScript"),
    new TopicOption("Both are awesomse!")
  ])
];

for (let i = 0; i < 100; i++) {
  const randomTopic = topics[Math.floor(Math.random() * topics.length)];
  const randomOption = randomTopic.options[Math.floor(Math.random() * randomTopic.options.length)];
  randomTopic.vote(randomOption.id);
}

topics.forEach(topic => {
  const winner = topic.getWinner();
  console.log(`Winner for '${topic.name}' is: '${winner.option.name}'!!!`);
})

You can think of this file scope as the Main() method of your .NET Console application. This is where you will be bringing all of your application together and execute appropriately.

Executing the application

Conclusion

I have shown you the tip of the iceberg here. The truth is that TypeScript actually gives you way more than this with handful of advanced type features such as Discriminated Unions and String Literal Types, which makes it possible to model complex scenarios in much more expressive and safe ways.

More on TypeScript

Want to read more about getting started with TypeScript? Check out these related posts:


tugberk-ugurlu-speaker-profile-vertical
About the Author

Tugberk Ugurlu

Tugberk Ugurlu is a self-started software engineer with 9 years of software development and technical leadership experience, who craves to create robust and accurate software products and build effective development teams. He is passionate about knowledge sharing and speaks, blogs, establishes collaboration on open source projects, and even authored a book. He works at Deliveroo in London and has been a Microsoft MVP for more than 6 years.

Related Posts

Comments

Comments are disabled in preview mode.