Latest

Behind the Code — Writing Modern JavaScript with TypeScript

Written by Thomas Reynolds and Alex Mahan

 

TypeScript is an open source 'typed superset' of JS that compiles down to plain JavaScript. It's made by Microsoft. Skeptical? Me too, at first. But after working with it for several months, I've come to really enjoy the features and future-friendly practices TypeScript encourages. Let's dig in and see how TypeScript can help JS devs become more productive and write more modular, maintainable code.

Types and JavaScript

JavaScript has six "primitive" types:

  • string
  • number
  • boolean
  • null
  • undefined
  • symbol
var string = 'a group of characters is a string';   // this is a string
var number = 2;   // this is a number
var isPizzaGood = true;   // this is a boolean
var nullVar = null;   // this is a null variable and represents the intentional absence of any object value
undefinedVar;   // this will return as undefined because you haven't defined the variable

Symbols are new to ES6 and you can read more about them on MDN, as we won't be discussing them here.

There is also the "Object" type, which is a data structure that contains data in the form of key/value pairs and can have properties, methods, etc. More info on data structure types in JS.

Loose Types

JavaScript is a loosely typed or dynamic language, which means that you don't have to declare the type of a variable ahead of time, and that the type of a variable can change:

var yourVariable = 300;         // yourVariable is a number
var yourVariable = "some text"; // yourVariable is now a string
var yourVariable = false;       // yourVariable is now a boolean

While this may appear to offer flexibility in building your site or app, this can also be dangerous. Because types aren't enforced, it's easy to accidentally change a string into a number, or a number into undefined, etc.

Additionally, JavaScript sees certain type values as "truthy" and others as "falsey". Examples of truthy values would be the value "true", "an actual string", 36, [1,2,4]. Falsey values would be the value "false", 0, '' (the empty string), null and undefined.

Depending on which equality operator used (== is the equals operator, === is the strictly equals operator), different values may be returned when comparing truthy and falsy values.

For example, if you attempt to compare a string to a boolean by entering '0' == false into a dev console, it returns true. Entering the same with the strict equals operator '0' === false returns false! While you shouldn't be attempting to compare a string to a boolean in the first place, this is a nightmare, because JS doesn't actually care what types you're comparing. Luckily, TypeScript can help us enforce consistent data types and avoid strange scenarios such at this. More on truth values in JS.

Typed JavaScript

TypeScript enforces types in your JavaScript. Typing is great. You can specify argument and return types. This means that you won't be able to get away with accidentally converting a string to a number. Or a number to a boolean. And if the return type of a method is specified as a string, you know that method will always return a string.

If you try to console.log('0' == false); in TypeScript (either with the equals or the strict equals operator), both the IDE and the TypeScript compiler will let you know that you cannot compare a string to a boolean:

error TS2365: Operator '==' cannot be applied to types 'string' and 'boolean'.
Typing your types

Setting types is quite straightforward:

const name: string = 'Nostradamus';
const age: number = 515;
const isAlive: boolean = false;
const isNotAlive = true;
const mainNav = document.querySelector('.main-nav') as HTMLElement;
const navElems = document.querySelectorAll('.nav-item') as HTMLElement[];

While you can be very explicit with your types, when assigning a value, TypeScript can look at the value and guess the type most of the time. Things like string, booleans and numbers are very easy to infer, so you can skip the type definition and just write the value (as in "isNotAlive" above).

As you can see, you can also type HTML elements. A "div" might be typed as a simple "HTMLElement" while an "input" element would be typed as "HTMLInputElement". TypeScript then checks to see if certain methods are valid on that element type. For instance, there are several special properties and methods available on an HTMLInputElement interface (placeholder, value, maxLength, etc) that aren't valid on a plain old "div".

Arrays are also typed. You can type an array of elements as above or just define an array of things to be filled later: "let myArray: any[]".

The "any" type can be used if you don't want to explicitly set a type on something.

If you are using ES6 classes, you must declare the types of your instance variables at the top of your class:

export default class YourClass {
  headlineElem: HTMLElement;
  navElems: HTMLElement[];
  inputElem: HTMLInputElement;
  isOpen = false;
  areEventsAttached = false;
  countryName: string;
  countryPopulation: number;
  anyThing: any;
  ...
}

You can also set types on your function arguments inline:

function createCountry(countryName: string, countryCode: number) {}

Read more on types in Typescript.

Interfaces

Interfaces are another great feature of Typescript. If you have a commonly repeated pattern, you can define your object's types as an object literal, then call that interface in other methods or objects:

interface Headline {
  url: string;
  language: string;
  country: string;
  population: number;
}

function renderItem(content: Headline) {
  console.log(
    content.url,
    content.language,
    content.country,
    content.population
  );
}

Read more on interfaces.

Type Definitions

This is where things get a little confusing.

When using TypeScript, you'll need to use TypeScript definition files in order to define and access third party JS modules and libraries within TypeScript. 

TypeScript definition files allow third party JS modules and libraries (such as jQuery, Greensock, React, Angular, etc) to be namespaced globally within your project, and allows those modules to be parsed properly by the TS compiler.

Until recently, you would install TSD to manage and install your TypeScript definition files, and a "tsd.json" file would be created in the root of your project to manage those dependencies.

However, TSD has been recently deprecated in favor of Typings. With Typings, you also can install external dependencies as needed and those dependencies will be managed in a "typings.json" file in your repo.

While there's hundreds of high quality type definition files available for TSD on the DefinitelyTyped repo, the list of type definition files for Typings is currently much shorter. You can continue to install type definitions from DefinitelyTyped by using the "--ambient" flag in Typings, or you can create your own type definition files as well.

Long story short: use Typings for managing your type definition files, and install type definitions from the DefinitelyTyped repo using the "--ambient" flag in Typings.

ES6/ES7 Features

TypeScript also comes with support for a bunch of useful features from ES6 and ES7. Fat arrow functions, classes (with support for optional type annotations), const, let, Promises, Decorators, rest parameters, spread operators, template literals, Symbols, Map, etc. Without going into depth here, the list is long and the support is robust.

IDE Support

The IDE support for TypeScript is quite good. In IDEs such as Visual Studio Code, Sublime, Atom, and Eclipse, you get error checking and code completion for free. Methods from classes will autocomplete, types are enforced, arguments are autocompleted, etc. It's super nice to be able to see warnings/errors as you code.

Learn More

Visit the official TypeScript website for documentation, code samples, and all the latest updates to TypeScript. It's definitely worth exploring if you're interested in writing modern, maintainable, typed JavaScript!

Related