Stay informed on COVID-19. Learn about the safety measures you can take.

Visit the CDC
  • Rally
  • TypeScript 101 Part 1

TypeScript 101 Part 1

By Brian Montana | February 10, 2020

Introduction

This post is a shallow discussion on the what, who, why, where, when of TypeScript. We’ll then step into practices, fundamentals, and advanced typing; each discussion will be surface level with examples, so let’s begin!


What, Who, Why, Where, When of TypeScript

What is TypeScript? It’s a strict syntactical superset of JavaScript, allowing optional static typing. All JavaScript projects are valid TypeScript but all TypeScript are not valid JavaScript.

Who was involved on TypeScript? TypeScript was developed and created by teams at Microsoft. It’s been designed for compatibility and scalability while keeping the future of ECMAScript proposals in sight.

Why use TypeScript? The type checking allows for a short feedback loop through an implicit, explicit, structural, and ambient type system. Think of it as a quick way to document your code in the immediacy, while highlighting possible errors that could exist at run time.

Where can you use TypeScript? You can start using TypeScript in Visual Studio Code because VS Code IS written in TypeScript. 

When can you use TypeScript? You can start by slowly adding types to your code. Let’s check out this example of code without TypeScript and after.

const times: number[] = [];
 
for (let i = 0; i < 10; i++) {
   times.push(Math.random() * 100000);
}
// TypeScript is compiled to JS
"use strict";
const times = [];
for (let i = 0; i < 10; i++) {
   times.push(Math.random() * 100000);
}

Migration

Migration can start by converting your *.js files to *.ts, and suppressing errors. If you use a different IDE or text editor you can use third party extensions for TypeScript support.

Dos and Don’ts

There are some common mistakes that can happen as you expand into TypeScript, this is helpful advice from the TypeScript team. Don’t use the Number, String, Boolean, Symbol, Object these are built-in objects; we should use number, string, boolean, symbol because they are basic types. Define the type you’ll expect in callbacks, don’t use a generic type like any in callbacks. Same advice, don’t use any as a response from a function's return. All parameters should be required and avoid optional parameters in callbacks. Overloading functions should include the maximum arity instead of multiple types to validate each path. When you have to define types for overloads make sure it’s ordered by most specific parameter requirements. Use optional parameters in functions after required parameters.

Type Inference

TypeScript can infer the type through variable definition, let word = 'hello'; assigns the type string because it’s the same as  let word: string = 'hello';. Users can see this logic applied to function returns, const addNumbers = (a: number, b: number) => a + b; TypeScript infers the function return because of the parameters being numbers and the return adding two numbers. Object inference is based on the shape of the model you have defined, we can’t update an object’s key value that has been defined as a string to a number or other data type.

const objectInference = {
 a: 10,
 b: "This is not a string"
};
 
objectInference.a = 20;
objectInference.a = 10;
objectInference.a = "That"; // type ‘“That”’ is not assignable to ‘number’

Avoid using any type as a data type, using the compiler option --noImplicitAny to throw errors when a user applies an any type to the code is a good start.

Type Annotations

In TypeScript we project our intentions of methods in the code with annotations, a small example appeared in the Type Inference section but let's expand on that with defined annotations. Here we’re setting up a method but being explicit in the function’s return const addNumbers = (a: number, b: number): number => a + b; or const addNumbers = (a: number, b: number): string => `${a + b}`;. Annotations include number, string, symbol, boolean as primitive types, a lot of these can be inferred by TypeScript. We have more complex types that extend beyond primitive types, Array is defined as const arr: [] = []; and can be used as number[], string[], boolean[]. Interfaces are special types in TypeScript they allow users to define the shape of the data explicitly, and a fundamental feature. Void is another special type that is used on a function when a type is not returned. Anyone who’s used Java might notice some similarity in the types.

export interface Person {
 name: string;
 location: string;
 age: number;
 life: (parent: string) => string;
 death: (deathAt: number) => void;
}

We have additional types like let example: undefined | null | never;, undefined and null aren’t helpful on their own but using them with other types help identify possible errors. Never is a type that defines an area of hopefully unreachable code, examples include infinite loop, thrown errors, and any function that doesn’t hit the implicit return.

const lolReturned: () => never = () => {
 throw new Error("cool cool cool");
};

Type Compatibility

TypeScript allows for unsound behavior, since we can’t know certain operations might be safe at compile time. We’ll cover some of these cases and why. We can start by looking at an interface being set in a function.

interface Human {
 name: string;
}
 
let y = { name: "Binish" ,location: "Chicago" };
 
function greet(n: Named) {
 console.log("Hello, " + n.name);
 console.log("Hello, " + n.location);
}
 
greet(y); // OK

You might notice the parameter is requesting an interface with a name property but allows an argument that includes location, this is part of the unsound behavior. TypeScript will only focus on the target type for compatibility, so if we try to access the location key from the parameter we’ll see an error.

Now we’ll look at comparing two functions with similar starting parameters, types, and return type. We can think about the example below as all squares are rectangles but not all rectangles are squares. The method with less parameters doesn’t have an error when overwriting the function with more parameters.

let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
 
x = y; // Error
y = x; // OK

We can think of common examples like array methods such as filter, reduce, forEach, and map that include multiple parameters, but aren’t required. Filter doesn’t require all the parameters below, but you can still access them.

array1.filter((el) => el < 5);
array1.filter((el, index) => el < 5);
array1.filter((el, index, array) => el < 5);

Classes work as interfaces and object literals, so you could use the class as an interface. So the two examples below differ in the constructor arguments but we’re only checking the static keys such as feet when validating if they are of a similar type.

class Animal {
 weight: number;
 constructor(name: string, height: number) {
 
 }
}
 
class Dog {
 weight: number;
 constructor(breed: string) {
 
 }
}
 
let animal: Animal;
let dog: Dog;
 
animal = dog;
dog = animal;

Functions

Named and anonymous functions can have types set. First we’ll look at named function with typing and then anonymous.

function add(x: number, y: number): number {
 return x + y;
}
 
let myAdd: (x: number, y: number) => number;
myAdd = function(x: number, y: number): number { return x + y; };

We’ll focus on the full function type, we can set the types in the variable to the right of the colon. It’s similar in the anonymous function we’re setting but we’ve simplified the typing, but the parameters are still aligned. We don’t need the type setting right of the equal in the anonymous function, we have “contextual typing” so we’re inferring the parameters will be number and the return will be a number.

Interfaces

The most important feature of TypeScript are interfaces, they allow you to set the shape of an object. We’ll show a simple example of an interface, and how errors can be detected.

interface Kiedis {
 cuteness: number;
}
 
function printPicture(kiedis: Kiedis) {
 console.log(kiedis.name);
 console.log(kiedis.cuteness);
}
 
let myObj = {cuteness: 10, name: "K"};
printPicture(myObj);

The quick feedback loop of TypeScript notifies the user that the key on this object is undefined and will cause an error if implemented. We can also set optional parameters on objects, it’s useful for “option bags”, a fundamental practice that allows users to pass all properties or only the required properties. The properties “paint?” and “color?” can be defined or not, but you can’t set properties not listed in the interface.

interface ThisIsNotAnObject {
 width: number;
 height: number;
 paint?: string;
 colors?: string[];
}
const notAPipe1: ThisIsNotAnObject = {
  width: 60,
  height: 40,
  colors: ['yellow', 'red']
}
const notAPipe2: ThisIsNotAnObject = {
  width: 60,
  height: 40,
  artist: 'René Magritte'
}

Classes

The structure of classes is familiar to people who’ve programmed in Java or C#. You’ll have the class name, constructor, properties, and methods; TypeScript inherits the OO patterns making assumptions that users will want to infer the same types of the class as it’s interface.

class Artwork {
 artist: string;
 createdYear: number;
 constructor(artist: string, createdYear: number) {
   this.artist = artist;
   this.createdYear = createdYear;
 }
 
 artistTitle() {
   return `${this.artist} created this piece in ${this.createdYear}`
 }
}

Now let’s look at extending that class. Extending Artwork on Sculpture will apply those same properties and types, so we get the benefit of type checking through inheritance and TypeScript will notify us when there’s an error with what we’re trying to access.

class Sculpture extends Artwork {
 medium: 'sculpture';
 constructor(artist: string, createdYear: number) {
   super(artist, createdYear);
   this.artist = artist;
   this.createdYear = createdYear;
 }
 
 artworkMedium() {
   return `This piece is a ${this.medium}.`
 }
}
 
let artwork = new Artwork('artistname', 1666);
artwork.artworkMedium();
 
let marble = new Sculpture('artistname', 1666);
marble.artworkMedium();

Advanced Types

Intersection &, union |, and type guarding. Intersection combines the properties of multiple types into a singular object. It’s useful when transforming data so the return value includes the combined properties with generics.

interface Artwork {
 height: number;
 width: number;
 depth: number;
}
 
interface Sculpture {
 medium: string;
 weight: number;
}
 
interface Painting {
 paint: string;
 colors: string[];
}
 
function transformArt<artwork, t="">(artwork: Artwork, type: T): Artwork & T {
 return {...artwork, ...type};
}
</artwork,>

Union types allow us to define this or that for the type. In the example we want to get the shape of an object’s interface as a Sculpture or Painting, both are possible depending on the argument we pass through the function.

function deconstructArt(artwork: Artwork): Sculpture | Painting {
 if(artwork.depth > 1) {
   return {
     type: 'sculpture',
     medium: 'marble',
     weight: 100
   }
 }
 return {
   type: 'painting',
   paint: 'oil',
   colors: ['red', 'blue', 'green']
 }
}

Conclusion

Thanks for checking out the first part of our TypeScript 101, a lot of the information is found on the documentation pages of TypeScript, Basarat’s gitbook, awesome-typescript repo, and from using TypeScript.

Resources

https://www.typescriptlang.org...
https://basarat.gitbooks.io/ty...
https://github.com/dzharii/awe...

Brian Montana