Effective TypeScript

Kito Mann (@kito99), Virtua, Inc.

Kito D. Mann (@kito99)

  • Principal Consultant at Virtua (http://virtua.tech)

  • Training, consulting, architecture, mentoring

    • JSF, Java EE, Polymer/Web Components, Angular

  • Official US PrimeFaces and PrimeNG partner

  • Author, JavaServer Faces in Action

  • Founder, JSF Central (http://www.jsfcentral.com)

Kito D. Mann (@kito99)

  • Co-host, Enterprise Java Newscast (http://enterprisejavanews.com)

  • Java Champion

  • Google Developer Expert in Web Technologies

  • Internationally recognized speaker

    • JavaOne, JavaZone, Devoxx, Devnexus, NFJS, etc.

  • JCP Member

    • JSF, MVC, JSF Portlet Bridge, Portlets

What about You?

Target Audience

  • Worked on a couple of TS projects

  • New to TypeScript

  • Leading a team on a TypeScript project

Why TypeScript?

  • JavaScript is pretty cool

  • …​but it is somewhat flawed

Why TypeScript?

  • TypeScript is "JavaScript that scales"

  • Open source (started and maintained by Microsoft)

  • Lead architect is Anders Hejlsberg (C#, Delphi, Turbo Pasal)

Why TypeScript?

  • Support for the ES5, ES6, and ES.next features

    • Transpiler like Babel, Traceur, or Clojure

  • An extensive type system built to work within the world of JavaScript

    • Allows code completion (IntelliSense), refactoring, and other features

    • Reduces errors

Effective use of TypeScript requires understanding both the newer JavaScript features and the TypeScript type system

ES6 / ES2015

Class syntax

class Shape {
	constructor(id, x, y) {
		this.id = id;
		this.x = x;
		this.y = y;
	}
}
class Rectangle extends Shape {
    constructor (id, x, y, width, height) {
        super(id, x, y)
        this.width  = width
        this.height = height
    }
}
class Circle extends Shape {
    constructor (id, x, y, radius) {
        super(id, x, y)
        this.radius = radius
    }
}

ES6 / ES2015

let and const

let a = [5,4,"foo",45];
for (let i = 0; i < a.length; i++) {
    let x = a[i];
}
console.log(a); // [5,4,"foo",45]
console.log(i); // undefined
console.log(x); // undefined
const immutable = 45;
immutable = 46; // error

ES6 / ES2015

Arrow functions

this.nums.forEach((v) => {
    if (v % 5 === 0)
        this.fives.push(v)
})

ES6 / ES2015

Promises

function msgAfterTimeout (msg, who, timeout) {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(`${msg} Hello ${who}!`), timeout)
    })
}
msgAfterTimeout("", "Foo", 100).then((msg) =>
    msgAfterTimeout(msg, "Bar", 200)
).then((msg) => {
    console.log(`done after 300ms:${msg}`)
})

ES6 / ES2015

Symbols

let f = Symbol("foo");
let f2 = Symbol("foo");
f === f2; // false

let f3 = f;
f === f3; // true

ES6 / ES2015

Template literals

let customer = { name: "Foo" }
let card = { amount: 7, product: "Bar", unitprice: 42 }
let message = `Hello ${customer.name}, want to buy ${card.amount} ${card.product} for a total of ${card.amount * card.unitprice} bucks?`
console.log(message)
// Output: Hello Foo, want to buy 7 Bar for a total of 294 bucks?

ES6 / ES2015

Destructuring

let { op, lhs, rhs } = getASTNode()
let tmp = getASTNode();
let op  = tmp.op;
let lhs = tmp.lhs;
let rhs = tmp.rhs;

ES6 / ES2015 / ES.next

Spread operator

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

const numbers = [1, 2, 3];

console.log(sum(...numbers));
// Output: 6

ES6 / ES2015 / ES.next

Spread operator

const numbers = [1, 2, 3];
const numbersClone = [...numbers]; // same as newNumbers.slice()
const numbersAndStrings = ['a', 'b', 'c', ...newNumbers]; // ['a', 'b', 'c', 1, 2, 3]
const widget = { weight: 50, length: 100, width: 100, color: black }
const clone = {...widget} // shallow copy; similar to Object.assign()
widget === clone; // false

ES6 / ES2015 / ES.next

Rest parameters

function log(sender, functionName, ...additionalInfo) {
  console.log("[LOG]","sender:", sender, "function:", functionName, "Additional info:", additionalInfo);
}

log("MyObj", "myFunc", "a", "b", "c");

// Output: [LOG] sender MyObj function myFunc Additional info (3) ["a", "b", "c"]

ES.next

Decorators

@component('prop-app')
export class App extends NowElements.BasicApp {

	/** Primary route object. */
	@property({
		type: Object,
		notify: true
	})
	route: any;

	/** Subroute (tail); for consumption by child pages. */
	@property({
		type: Object,
		notify: true
	})
	subroute: any;

	@property({type: Object})
	routeData: any;

	@property({type: String})
	reqNum: string;

    ...

	@observe('reqNum')
	_loadRequisition(reqNum: string) {
		console.debug(this.is, '_loadRequisition', reqNum);
		if (reqNum && reqNum) {
			this._addType = AppType.Requisition;
			this.$.getReqInfo.params = {filter: "ReqNum=" + reqNum};
			this.$.getReqInfo.generateRequest();
		}
	}

	@listen(BasicAppLayout.SEARCH_EXECUTED_EVENT)
	private _search(evt: PolymerEvent) {
		let query = evt.detail;
		if (this._addType === AppType.Project) {
			if (!this.queryParams) {
				this.queryParams = new QueryParams();
			}
			this.set('queryParams.projectQuery', query);
			this.navigateToRoute('/project-list');
		} else {
			this.set('queryParams.reqQuery', query);
			this.navigateToRoute('/requisition-list');
		}
	}
...
}

ES.next

Decorators

// @component class decorator
function component(tagname, extendsTag) {
    return function (target) {
        target.prototype["is"] = tagname;
        if (extendsTag !== undefined) {
            target.prototype["extends"] = extendsTag;
        }
    }
}

Decorators are called at run-time, and can modify whatever they are decorating.

ES.next

Decorators

// @listen method decorator
function listen(eventName) {
    return (target, propertyKey, descriptor) => {
        target.listeners = target.listeners || {};
        target.listeners[eventName] = propertyKey;
    }
}

TypeScript also supports accessor, property, and parameter decorators

Other features (ES6/7/8/Next)

  • Modules

  • Async / await

  • Generators and Iterators

  • Maps and Sets

  • Proxying and Reflection

  • Internationalization / Localization

Don’t forget accessors and mutators (ES5)

get name() {
    return this._name
}

set name(name) {
    this._name = name;
}

get showPanel() {
    return this._name && this.active && this.loadComplete;
}

Understand truthy / falsy

Falsy

if (false)
if (null)
if (undefined)
if (0)
if (NaN)
if ('')
if ("")
if (``)

Every thing else is truthy

OK, So what about TypeScript?

TypeScript: transpiler

  • Outputs JavaScript

  • Can target ES3-ES.next

  • Type system does not affect runtime

Demo

JavaScript types

  • Primitives

    • null, undefined, number, string, boolean, symbol, BigInt (es.next)

  • Objects

    • Includes Function and Array

JavaScript types

A variable can be re-assigned to any primtive or object

let a = 1;
a = 'foo';
a = { color: 'red', size: 12 };

TypeScript types

  • Every variable has a type (which may be inferred)

  • Rules govern which types may be assigned to each other

  • Fully supports generics

TypeScript types

null and undefined

let name: string; // default is undefined
name = "Kito";
name = null; // OK
name = undefined; // OK

// with --strictNullChecks

let name = "Kito"; // must be initialized
name = null; // Error
name = undefined; // Error

TypeScript types

any

let foo: any; // same as "let foo;"

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.

let list: any[] = [1, true, "free"];

list[1] = 100;

TypeScript types

void

function log(sender: any, functionName: string,
    ...additionalInfo: any[]): void {
  console.log("[LOG]","sender:", sender,
      "function:", functionName, "Additional info:", additionalInfo);
}

TypeScript types

never

// Function returning never must have unreachable end point
function error(message: string): never {
    throw new Error(message);
}

// Inferred return type is never
function fail() {
    return error("Something failed");
}

// Function returning never must have unreachable end point
function infiniteLoop(): never {
    while (true) {
    }
}

TypeScript types

unknown

  • Type-safe version of any

let x: unknown = 5;
x == 5; // true
x !== 10; // true
x >= 0;  // Error
x + 1;  // Error
x * 2;  // Error
-x;  // Error
+x;  // Error
x.foo(); // Error

x as number * 2; // 10

declare function isFunction(x: unknown): x is Function;

function f20(x: unknown) {
    if (typeof x === "string" || typeof x === "number") {
        x;  // string | number
    }
    if (x instanceof Error) {
        x;  // Error
    }
    if (isFunction(x)) {
        x;  // Function
    }
}

TypeScript types

enum

enum Direction {
    Back,
    Forwards,
    Left,
    Right = 10
}

console.log(Direction.Back); // 0
console.log(Direction.Right); // 10
console.log(0 === Direction.Back); // true

class Walker {
    walk(dir: Direction) {
        switch (dir) {
            case Direction.Back: console.log('Walk Back'); break;
            case Direction.Forwards: console.log('Walk Forwards'); break;
            case Direction.Left: console.log('Walk Left'); break;
            case Direction.Right: console.log('Walk Right'); break;
        }
    }
}

const joe = new Walker();
joe.walk(Direction.Left); // "Walk Left"
joe.walk(Direction.Right); // "Walk Right"

// same result as switch stattement
class Walker {
    walk(dir: Direction) {
        console.log(`Walk ${Direction[dir]}`);
    }
}

TypeScript types

enum

enum Messages {
    Confirmation = 'Are you really sure you want to do that?',
    Error = 'Uh oh. Something bad happened...',
    Welcome = 'Welcome to our wonderful app.'
}

console.log(Messages.Confirmation); // Are you really sure you want to do that?
console.log('Welcome to our wonderful app.' === Messages.Welcome); // true

TypeScript types

tuple

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
x = [10, "hello"]; // Error

// When accessing an element with a known index, the correct type is retrieved:

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'

// When accessing an element outside the set of known indices, a union type is used instead:

x[3] = "world"; // OK, 'string' can be assigned to 'string | number'

console.log(x[5].toString()); // OK, 'string' and 'number' both have 'toString'

x[6] = true; // Error, 'boolean' isn't 'string | number'

TypeScript types

intersection

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U>{};
    for (let id in first) {
        (<any>result)[id] = (<any>first)[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            (<any>result)[id] = (<any>second)[id];
        }
    }
    return result;
}

class Person {
    constructor(public name: string) { }
}
interface Loggable {
    log(): void;
}
class ConsoleLogger implements Loggable {
    log() {
        // ...
    }
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

TypeScript types

union

function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${padding}'.`);
}

let indentedString = padLeft("Hello there!", '    ');
indentedString = padLeft("Hello there!", 000000);
indentedString = padLeft("Hello world", true); // error

TypeScript types

union

class FancyPanel {

    style: 'accordion' | 'collapsible' | 'normal';

    init() {
        if (this.style === 'accordion') {
            ...
        } else
        if (this.style === 'collapsible') {
            ...
        } else
        if (this.style === 'normll') { // error
            ...
        }
    }
}

TypeScript types

index

interface Person {
    name: string;
    age: number;
}
let person: Person = {
    name: 'Bill Gates',
    age: 63
};
let personKeys: keyof Person; // name | age

function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
  return names.map(n => o[n]);
}

let strings: string[] = pluck(person, ['name']); // ok, string[]

TypeScript type system

  • Structural type system

  • All TypeScript types are transpiled to JavaScript types at runtime

  • Considered "unsound"

TypeScript type system

Basic rule: if x is compatible with y if x has at least the same members as x

interface Weighed {
    weight: number;
}

let x: Weighed;
// y's inferred type is { name: string; weight: number; }
let y = { name: "Mickey", weight: 400 };
x = y;
y = x; // error

class Widget {
    name: string;
    weight: number;
    color: string;
}

let w: Widget;
x = w;
w = x; // error

function widgetPackager(widget: Widget) { };

widgetPackager(w);
widgetPackager({ name: "Mouse", weight: 500, color: "red"});
widgetPackager(x); // Error

Types for external libraries

Using types isn’t all that useful if you don’t use them for external libraries

  • TypeScript provides types for non-TypeScript libraries through declaration (d.ts) files

  • Ships with types for libraries for the output target

    • ES3, ES5, ES6, etc.

Types for external libraries

  • Many libraries include them, but you can install them if they don’t

    • npm install --save @types/lodash

    • import * as _ from "lodash";

    • _.padStart("Hello TypeScript!", 20, " ");

  • TypeScript declaration file search: https://aka.ms/types

Types for external libraries

  • If there are no existing libraries, declare it:

declare const myLibrary: any;

myLibrary.doSomething();

Compiler options

  • Can be specified in tsconfig.json file (preferred) or command line

  • Recommended options:

  • --experimentalDecorators

  • --target ("ES3" (default), "ES5", "ES6"/"ES2015", "ES2016", "ES2017" or "ESNext")

  • --noEmitOnError

Compiler options: Be strict!

  • --strict enables:

    • --noImplicitAny

    • --noImplicitThis

    • --alwaysStrict

    • --strictBindCallApply

    • --strictNullChecks

    • --strictFunctionTypes

    • --strictPropertyInitialization

Bottom line: effective TypeScript requires

  • Understnding new JavaScript features

  • Using TypeScript’s type system and other language features

  • Setting up your project to be as strict as possible

Questions?