TypeScript

What is it?

TypeScript, the language, is a strict superset of JavaScript ES6+ that adds a type system to JavaScript. It keeps all of the JavaScript syntax and features.

What TypeScript adds is the syntax to describe types of data, objects, classes, and functions.

What is a type?

According to wikipedia

A data type is a classification of data which tells the compiler or interpreter how the programmer intends to use the data.

Why should you care about types?

Type checking, the process of validating matching types in your programs, removes an entire category of bugs from your program by not allowing you to make them. Essentially, a type system is there to help you ensure correct code without even running your program.

JavaScript with Types

JavaScriptTypeScript

var a = 1;
var b = true;
var c = "test";
var d = null;
var e = undefined;
var f = [1,2,3];
var g = ["one","two"];
var h = new Date();
var i = Symbol("foo")
                    

var a: number = 1;
var b: boolean = true;
var c: string = "test";
var d: null = null;
var e: undefined = undefined;
var f: number[] = [1,2,3]
var g: string[] = ["one","two"]
var h: Date = new Date();
var i: symbol = Symbol("foo")
                    

What happens when we get things wrong


var a: number = "test";
var b: boolean = 3;
var c: string = false;
                    
example.ts|1 col 5 error| Type 'string' is not assignable to type 'number'.
example.ts|2 col 5 error| Type 'number' is not assignable to type 'boolean'.
example.ts|3 col 5 error| Type 'boolean' is not assignable to type 'string'.
                    

Any Type

The TypeScript type system supports gradual typing, in other words, it will let you add types as need. The "any" type allows you to put anything inside. It is an escape hatch from the type system. TypeScript also has syntax for type casting if needed. Ideally, they should be used sparingly.


var a: any = 1;
var b: any = true;
var c: any = "test";
var d: any = null;
var e: any = undefined;
var f: any[] = [1,2,3]
var g: any = ["one","two"]
                    

functions


function sum(x, y) {
    return x + y;
}
                    

function types


function sum(x: number, y: number): number {
    return x + y;
}
                    

objects


var point1 = {
    x: 1,
    y: 4,
    name: "start"
};
                    

object types


var point1: {x: number, y: number, z?: number, name: string} = {
    x: 1,
    y: 4,
    name: "start"
};
                    

The ? after property names indicates the property is optional

Type Aliases


type Point = {x: number, y: number, z?: number, name: string};
var point1: Point = {
    x: 1,
    y: 4,
    name: "start"
};
var point2: Point = {
    x: 4,
    y: 10,
    z: -1,
    name: "end"
};
                    

String Literal Types

You can treat string constants as types.


type PrimaryColor = "red" | "blue" | "green";
function adjustColor(color: PrimaryColor, value: number) { /* */ }
adjustColor("red", 123) //ok
adjustColor("purple", 255) //Error
                    

Enums

You can create named numeric contants with enums


enum Directions {
    Up=1,
    Down,
    Left,
    Right
}

function Go(d: Directions): string {
    return "You went " + Directions[d] + " aka " + d;
}

Go(Directions.Left); //You went left aka 3
                

Numeric Literal Types


type EightBit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
let coolbit: EightBit = 7; //ok
let badbit: EightBit = 8; //error
                    

Type Aliases

Type aliases can be used to alias primitives, which can be useful for documentation


type Dollars = number;
type Name = string;
function BankBalance(person: Name): Dollars {
    return 3.50
}
                    

Function Type Alias

Functions can also be described with type aliases


function sum(x: number, y: number): number {
    return x + y;
}

type binary_op = (x: number, y: number) => number;
var sum2: (x: number, y: number) => number = sum;
var sum3: binary_op = sum;
                    

Classes

Typescript supports the es6 syntax for classes


class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
class SoftwareEngineer extends Person {
    prog_language: string;
    constructor(name: string, prog_language: string) {
        super(name)
    }
}
var joeTester: Person = new SoftwareEngineer("Joe", "TypeScript");
                    

Class syntax

Typescript simplifies the "this.property = property" pattern with the public keyword. It also supports private and protected attributes on classes, but these are compiler enforced only.


class Person {
    constructor(public name: string) {}
}
class SoftwareEngineer extends Person {
    private prog_language: string;
    constructor(public name: string, private prog_language: string) {
        super(name)
        this.prog_language = prog_language;
    }
}
                    

Abstract Classes

Abstract classes are base classed which are meant to be inherited from. They cannot be instantiated directly. They specify methods which must be implemented in the subclasses. They are allowed to include implementations of methods to share as well.


abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}
class Mammal extends Animal {
    makeSound(): void {
        console.log("A noise!");
    }
}
                    

Interfaces

Interfaces can be used to describe objects, classes, and functions. They are similar to abstract classes however, they cannot contain an implementation of a function. Interfaces fill a similar role to type aliases, but they can do and describe a few more things.


//object type
interface Point {
    x: number;
    y: number;
    z?: number;
    label: string;
}
var origin: Point = {x: 0, y: 0, z:0, label: "origin"};
//function type
interface binary_op {
    (x: number, y: number): number
}
var add_one: binary_op = function(x: number, y: number): number {
    return x + y + 1
};
                    

Classes implementing interfaces

Interfaces can ensure that a class must implement specific methods and attributes.


interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}
class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(hour: number, minute: number) {}
}
                    

Indexable types

Interfaces can also be used to describe container objects like arrays and objects


interface PhoneBook {
    [phoneNumber: string]: Person
}
class Person {
    constructor(public name: string) {}
}
var yellowpages: PhoneBook = {
    "1112223333": new Person("John"),
    "3334445555": new Person("Jane")
}

interface GuestList {
    [index: number]: Person
}
var partylist: GuestList = [new Person("John"), new Person("Jane")]
                    

Immutable data

Variables, classes, and interfaces can enforce immutability on variables and properties. TypeScript also comes with a ReadonlyArray< T > type. Only the compiler forbids reassignment.


const example = true;
interface ImmutablePoint {
    readonly x: number;
    readonly y: number;
}
class Pet {
    constructor(public readonly name: string) {};
}
let hex_letters: ReadonlyArray< string > = ['A','B','C','D','E','F'];
                    

Tuple Types

Tuples are a way to describe fixed length arrays with types for each index.


let person: [string, number] = ["John Smith", 30];
person = ["Jane Doe", 30]; //ok
person = [20, "Joe Tester"] // Error
                    

Union Types

We can use union types to describe pieces of data that can be two or more types of data. The | operator lets us create a union of types.


var password: string | number = "test"
password = 1234 //also ok!

function validatePassword(password: number | string): boolean {
    if(typeof password == "number") {
        return password > 1000;
    }else{
        return password.length > 4;
    }
}
                    

Type Guards

Once you have the ability to mash types together with Union Types, TypeScript also gives us the ability to pull types apart with type guards.


class BinaryPassword {
  data: ArrayBuffer;
  constructor(public data: ArrayBuffer) {}
}
type EncryptedPassword = {password: string};
function IsEncryptedPassword(obj: Any): obj is Password {
    return obj.password !== undefined
}

type Password = BinaryPassword | EncryptedPassword | number | string;
function validatePassword(password: Password): boolean {
    if(password instanceof BinaryPassword) {
        return true;
    }else if(IsEncryptedPassword(password)) {
        return true;
    }else if(typeof password == "number") {
        return number > 1000;
    }else if(typeof password == "string" ){
        return string.length > 4;
    }else{
        throw new TypeError("Not a valid password type");
    }
}
                    

Intersection Types

These allow you describe an object which is one type AND another type. A common JavaScript pattern called the "mixin" pattern can be described with intersection types.


type Fahrenheit = {fahrenheit: number};
type Celcius = {celcius?: number};
function convertToCelcius(temp: Fahrenheit): Fahrenheit & Celcius {
    let result: Fahrenheit & Celcius = temp;
    let c_temp = (temp.fahrenheit-32)*(5/9);
    result.celcius = c_temp;
    return result;
}
                    

Structural Typing

Most languages traditional languages use a nominal type system, which means that when they compare types, they look at the name of the type to determine if an expression is well typed.

Typescript has a structural type system which means that it compares the shape of types rather than just the name.

Structural Typing example


type Person = {name: string, age: number};
type Pet    = {name: string, age: number, breed: string};

function HappyBirthday(p: Person) {
    p.age++;
    console.log(`Happy Birthday ${p.name}!`+
        `You are now ${p.age} years old!`);
}
let myDoge: Pet = {name: "Spot", age: 4, breed: "pug"};
HappyBirthday(myDoge);
//Happy Birthday Spot! You are now 5 years old!
                    

Tsconfig

Typescript has a config file you can generate for your project called "tsconfig.json". Among other things, this file allows you to ramp up or ramp down strictness in typing. Here is an example of a maximally strict config file.


{
    "compilerOptions": {
        "module": "commonjs",
        "sourceMap": true,
        "target": "ES6",
        "noImplicitAny": true,
        "noImplicitReturns": true,
        "noImplicitThis": true,
        "noFallthroughCasesInSwitch": true,
        "strictNullChecks": true
    }
}
                    

Control Flow Analysis

Typescript can also prevent common errors because it understands how data and types flow through JavaScript.


var test: number;
console.log(test); //error unassigned variable

function goTime(isGoTime: boolean) {
    if(isGoTime) {
        return isGoTime;
    }else{
        return !isGoTime;
    }
    return "asdf"; //error unreachable code
}

switch(test) {
    case 1:
        console.log("Hello world!"); //error switch case fall-through
    case 2:
        console.log("Goodnight world!");
        break;
    default:
        console.log("foobar");
}
                    

Discriminated Unions

TypeScript has the ability to create complex types called a discriminated unions. Combining literal types or type guard functions TypeScript can narrow a union type into a specific type to define per type behavior.


interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
type Shape = Square | Rectangle | Circle;
function area(s: Shape) {
    switch (s.kind) {
        case "square": return s.size * s.size;
        case "rectangle": return s.height * s.width;
        case "circle": return Math.PI * s.radius ** 2;
    }
}
                    

Generic (aka Parametric) Types

These functions take a type as a parameter. A is a variable. Interfaces, classes, and type aliases all support generics


function head< A >(xs: A[]): A { return xs[0]; }

function tail< A >(xs: A[]): A[] { return xs.slice(1); }
let one = head< number >([1,2,3]);
let letter = head< string >(["a","b"]);
type Container< A > = { value: A };

class LinkedList< A > {
  data: A;
  next: LinkedList< A >;
  constructor(public value: A, public next: LinkedList< A >) { }
}
                    

Generic Type Constraints

It is possible to add constraints to generic types.


interface Lengthwise {
  length: number;
}
function logLength< T extends Lengthwise >(arg: T): T {
    console.log(arg.length);
    return arg;
}
                    

Lookup Types

A common pattern in JavaScript is to use indirection when working with objects of a certain kind .


inteface Person {
    name: string;
    age: number;
}
let person = {name: "Joe Tester", age: 30};
let personProp: keyof Person = "name"; //ok
let personProp2: keyof Person = "test"; //error
function getProperty< T, K extends keyof T >(p: T, prop: keyof K): T[K] {
    return p[prop];
}
let age: number = getProperty(person, "age");
                    

Additional Topics

  • void, and never types
  • Function type aliases, subtypes, and variance
  • Mapped types
  • Modules
  • static class methods
  • JSX

The Goal of a Type System

The goal of a type system is to ensure that our program is free of bugs. However, to actually get this benefit takes a bit of design. The trick is to push business logic into the types. The goal is to make invalid states inexpressible. If you have an expressive type system, it becomes easier to describe your business logic as types and thereby create code that is provably correct.

Setup


sudo npm install -g typescript
                    

installs the command tsc

How do you use it?


tsc filename.ts
                    

Upon running tsc on a ts file it will generate a .js file of the same name, which is the translation of the typescript code

Resources