Typescript tutorial

Overview

  • Provide an optional type system for JavaScript.
  • Provide planned features from future JavaScript editions
  • Target is ES5 (browser supported)
  • Usually Type errors do not prevent JavaScript emit

Interface

interface Point2D {
    x: number;
    y: number;
}

var point2D: Point2D = { x: 0, y: 10 }
function iTakePoint2D(point: Point2D) { /* do something */ }

Interface that accepts additional keys:

interface Model {
   name: string;
   [key: string]: any;  // Or [key: string]: unknown;
   [key: number]: any;
} 

Note: We should have had a reserved word or built-in type like "base interface" which allows additional properties since it is a common use case.

unknown vs any type

  • any type completely disables type checking when involved in either rhs or lhs of an operation.
  • unknown is a natural type which delays type checking. It accepts any value, but later it restricts operation to only that type. It is preferred over any.
  • Anything is assignable to unknown.
  • unknown type variable is not assignable to any fixed type variable (without typecast like "as string" etc.)
  • Even after assigning another fixed type value to unknown variable, no operation can be performed on unknown variable. You need to assign this to a fixed type variable (using typecast), then only you can access any properties, etc.
let vAny: any = 10;          // We can assign anything to any
let vUnknown: unknown =  10; // We can assign anything to unknown just like any

typeof vUnknown === 'number'  // True.

let uk:unknown;
typeof uk === 'undefined'     // True

let s1: string = vAny;     // Any is assignable to anything
let s2: string = vUnknown; // Invalid; we can't assign vUnknown to any other type (without an explicit assertion)

vAny.method();     // Ok; anything goes with any
vUnknown.method(); // Not ok; we don't know anything about this variable

vUnknown = 'hello';  // OK to reuse "unknown" variable with another type object.
typeof vUnknown === 'string'  // True.

Enum Types

  • Enum is numeric by default and first one assumes 0 value.
  • Enum can have string values also.
  • Enum can have mixed type values also.
enum Direction {
  Up = 1,       // By default first one gets value 0.
  Down,         // Automatically gets value 2 etc.
  Left,
  Right,
}

enum Direction { Up = "UP", Down = "DOWN" } 
// Defines:  Direction = { 0: "UP", 1: "Down", "UP":0, "Down": 1 }

const enum Enum { A = 1, B = A * 2 } // const enums are inlined and removed.

Typescript migration

  • For quick and dirty migration for 3rd party library, you can do:

    declare var $: any; // Entire jquery library, won't complain now. // You can include ambient type definition for 3rd party library // from DefinitelyTyped github repository JQuery.d.ts file.

  • Another way to deal with 3rd party npm module is to delcare that as module:

    declare module "jquery";
    import * as $ from "jquery";
    
    // External non js resources
    declare module "*.css";
    
    // Now you can ...
    import * as foo from "./some/file.css";
    
    declare module "*.html";
    
  • Another even better way is to install type defintion from DefinitelyTyped repo:

    npm install @types/jquery --save-dev
    // By default the global definitions get polluted. $ is immediately available.
    
  • If you want to selectively enable global types, edit tsconfig.json:

    { "compilerOptions": { "types" : [ "jquery" ] } }
    
  • You can selectively import @types using module:

    import * as $ from "jquery";
    // Use $ at will in this module :)
    
  • Insert all your "declare ..." statements in global.d.ts and vendor.d.ts file. Every statement in this file should start with declare so that no code is emitted after compilation. Author has to make sure, it is available during runtime.

  • For example:

    declare var process: any; // Or import community maintained node.d.ts

  • Typescript compiler comes with global lib.d.ts file which defines declration for DOM, for variables like window, document, math, etc.

  • You can target es5 but use es6 and dom for type assist :

    tsc --target es5 --lib dom,es6

    // tsconfig.json
    "compilerOptions": { "lib": ["dom", "es6"] }

  • Polyfill for old JavaScript engines :

    npm install core-js --save-dev
    // Add following in your application entry point:
    import "core-js";
    

declare statement

$('.awesome').show(); // Error: cannot find name `$`

declare var $: any;
$('.awesome').show(); // Okay!

declare const fooSdk : { doSomething: () => boolean }
fooSdk.doSomething();  // fooSdk is global variable. Now Okay!

declare let module: any; // suppress errors for module.xxx access.
let module: any;         // Error! module already defined!
  • declare is used to tell the compiler "this thing exists already".
  • There is no need to compile this statement into any JavaScript.
  • Useful for type inference.
  • We call declarations as “ambient”. Typically these are defined in .d.ts files.

Importing from javascript module:

// getMyName.js
module.exports = function getMyName(name) {
    return name;
}
Then you import it into your typescript code

// myCode.ts
import getMyName from 'getMyName'; //getMyName is a JS module not typescript

When importing that JS module(allowJs compiler option should be set to false 
otherwise, JS files will be resolved without any type definitions), 

// Could not find a declaration file for module '/absolute/path/to/getMyName.ts'

// Provide type definition file now!

// getMyName.d.ts
declare function getMyName(name: string): string;

export default getMyName;    // Now Okay!

Type declaration vs Assertions

interface Options {
    x?: string;
    y?: number;
}

// Error, no property 'z' in 'Options'
let q1: Options = { x: 'foo', y: 32, z: 100 };

// "as Options" is called type assertion.
// Forces to accept extra property present.
let q2 = { x: 'foo', y: 32, z: 100 } as Options;

// Still an error (good):
// Note: x is type incompatible. Type assertion does not tollerate it!
let q3 = { x: 100, y: 32, z: 100 } as Options;

type union and intersections

type union:

// This is a dog or a cat or a horse, not sure yet
interface Animal { move; }
interface Dog extends Animal { woof; }
interface Cat extends Animal { meow; }
interface Horse extends Animal { neigh; }

let x: Animal;
if(...) {
  x = { move: 'doggy paddle', woof: 'bark' };
} else if(...) {
  x = { move: 'catwalk', meow: 'mrar' };
} else {
  x = { move: 'gallop', neigh: 'wilbur' };
}

type intersection is similar to interface extends:

interface DataModelOptions {
  name?: string;
  id?: number;
}
interface UserProperties {
  [key: string]: any;
}

function createDataModel(model: DataModelOptions & UserProperties) {
 /* ... */
}

// OK. model additional properties accepted.
createDataModel({name: 'my model', favoriteAnimal: 'cat' });

// Note: 

Type vs Interface

interface Client { 
  name: string; 
  address: string;
}
// We can express the same Client contract definition using type annotations:

type Client = {
  name: string;
  address: string;
};

// Note: type syntax involves using "=".
// Both above interface and type are synonymous.
  • Many times type and interface is synonymous.
  • type can deal with primitive types but interface is only for object/func type.
  • Union types better expressed with type.
type Address = string;

type NullOrUndefined = null | undefined;  // Union type alias

type Transport = 'Bus' | 'Car' | 'Bike' | 'Walk';  // Like Enum type

type AddFn =  (num1: number, num2:number) => number;

interface IAdd { (num1: number, num2:number): number; } // Similar to AddFn
// Note: AddFn looks better than IAdd.

// Generic (parameterized) Type:

type Car = 'ICE' | 'EV';
type ChargeEV = (kws: number)=> void;
type FillPetrol = (type: string, liters: number) => void;

type RefillHandler<A extends Car> = A extends 'ICE' ? FillPetrol :
                                    A extends 'EV' ? ChargeEV : never;

const chargeTesla: RefillHandler<'EV'> = (power) => {
    // Implementation for charging electric cars (EV)
};

const refillToyota: RefillHandler<'ICE'> = (fuelType, amount) => {
    // Implementation for refilling internal combustion engine cars (ICE)
};

// Note: never is an impossible type.

interface Client { name: string; }
interface Client { age: number; }  // Ok. Interface declarations are merged.

type Client = { name: string; }
type Client = { age: number; }  // Error! type redefined!

// Below both interface and type are similar. extends vs intersection.
interface VIPClient extends Client { benefits: string[] }
type VIPClient = Client & {benefits: string[]};

// When extending interfaces, the same property key isn’t allowed.
// But type intersection has complex semantics.

type Person = {
  getPermission: (id: string) => string;
};

type Staff = Person & {
  getPermission: (id: string[]) => string[];
};

const AdminStaff: Staff = {
  getPermission: (id: string | string[]) =>{
    return (typeof id === 'string'?  'admin' : ['admin']) as string[] & string;
  }
}

// Note: Type intersection creates union type for same property.

// You can implement class using interface or type. Both okay:
interface Person { name: string; greet(): void; }

class Student implements Person {
  name: string;
  greet() {
    console.log('hello');
  }
}

type Pet = { name: string; run(): void; };

class Cat implements Pet {  ... }

// Tuple type is supported:
type TeamMember = [name: string, role: string, age: number];
const peter: TeamMember = ['Harry', 'Dev', 24];

// You can have static value in type:
type NetworkFailedState = {
  state: "failed";
  code: number;
};
type NetworkLoadingState = {
  state: "loading";
};
...
type state = NetworkFailedState | NetworkLoadingState;

let myState:state;
// Okay. myState.code access is OK due to static code analysis!!!
if (myState.state === 'failed') console.log(myState.code);

See Also: https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html

// Type inference ...
const button = document.getElementById('my-element')
// Using type inference it assumes the button type is: HTMLElement | null

// You know that it is of type button element ...
const button = document.getElementById('my-element') as HTMLButtonElement

// You just know the result is not null. Then just add ! at the end:
const element = document.getElementById('my-element')!
// and element will have type HTMLElement.

type Casting

interface Animal { move; }
interface Dog extends Animal { woof; }
interface Cat extends Animal { meow; }
interface Horse extends Animal { neigh; }

let x:Animal;
let y:Animal;

x = <Dog>y;    // Typecast to Dog. Deprecated syntax due to JSX conflict.
x = y as Dog;  // Preferred way for casting.

let k = 10;
let s = k as string;   // Error! Types don't overlap for cast to succeed!

Note: Basically, the assertion from type S to T succeeds
if either S is a subtype of T or T is a subtype of S.

User defined type guard and type predicate

// Following function is a user defined type guard.

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();

if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}

// Using traditional alternative approach, you can do this ...
let pet = getSmallPet();
let fishPet = pet as Fish;
let birdPet = pet as Bird;

if (fishPet.swim) {     // Or check: ((pet as Fish).swim)
  fishPet.swim();
} else if (birdPet.fly) {
  birdPet.fly();
}

// Using "in" Operator is another preferred way of type narrowing.

function move(pet: Fish | Bird) {
  if ("swim" in pet) {
    return pet.swim();
  }
  return pet.fly();  // Deducts if "swim" not there, "fly" must be there!
}
// Above is pretty cool and mostly javascript compatible as well.
  • Typescript recognizes the if (typeof myvar === 'number') check for static code analysis and assumes myvar is of type number, etc.
  • Similarly, typescript recognizes the check for: myvar instanceof myClass also.
  • So you may avoid user defined type guards for most purposes.

Null Checks

By default, you can assign null and undefeined to any variable. This can lead to errors.

The strictNullChecks flag (in .config file) fixes this:

let exampleString = "foo";
exampleString = null;

// Type 'null' is not assignable to type 'string'.

let stringOrNull: string | null = "bar";
stringOrNull = null;

stringOrNull = undefined;
Type 'undefined' is not assignable to type 'string | null'.

Generic Types

type Container<T> = { value: T };
We can also have a type alias refer to itself in a property:

type Tree<T> = {
  value: T;
  left?: Tree<T>;       // Note: Recursive reference to Type!
  right?: Tree<T>;
};

type LinkedList<Type> = Type & { next: LinkedList<Type> };

Generic functions are mainly used for type inference of return values:

function reverse<T>(items: T[]): T[] {
    var toreturn = [];
    for (let i = items.length - 1; i >= 0; i--) {
        toreturn.push(items[i]);
    }
    return toreturn;
}

var sample = [1, 2, 3];
var reversed = reverse(sample);
console.log(reversed); // 3,2,1

// Safety!                           cols
reversed[0] = '1';     // Error!
reversed = ['1', '2']; // Error!

reversed[0] = 1;       // Okay

// Note: Array has built-in reverse defined. This is just to illustrate.
// Static code analysis detects the function arg type and derives return type.

Generic class:

/** A class definition with a generic parameter */
class Queue<T> {
  private data = [];
  push(item: T) { this.data.push(item); }
  pop(): T | undefined { return this.data.shift(); }
}

/** Again sample usage */
const queue = new Queue<number>();
queue.push(0);
queue.push("1"); // ERROR : cannot push a string. Only numbers allowed

JSX

  • Typescript supports JSX for transpilation and code analysis.
  • JSX is an XML-like syntax extension to ECMAScript without any defined semantics.
  • It's NOT intended to be implemented by engines or browsers.
  • It's NOT a proposal to incorporate JSX into the ECMAScript spec itself.
  • It's intended to be used by various preprocessors (transpilers) to transform these tokens into standard ECMAScript.
  • The main consumer of JSX at this point is ReactJS from facebook.
  • React can either render HTML tags (strings) or React components.
  • <div> ... </div> is recognized as HTML due to first letter being small letter.
  • <MyComponent> .... </MyComponent> recognized as React Component since first letter is capital letter.
const MyComponent: React.FunctionComponent<Props> = (props) => {
  return <span>{props.foo}</span>
}

<MyComponent foo="bar" />
  • Type of MyComponent is either an instance of Function (i.e. React.FunctionComponent) or a class (React.Component).
  • The type of <MyComponent /> is a higher level type: React.ReactElement<MyComponent>
class MyAwesomeComponent extends React.Component {
  render() {
    return <div>Hello</div>;
  }
}

const foo: React.ReactElement<MyAwesomeComponent> = <MyAwesomeComponent />; //Ok
const bar: React.ReactElement<MyAwesomeComponent> = <NotMyAwesomeComponent />; // Error!

Non-null assertion operator

// Compiled with --strictNullChecks
function validateEntity(e?: Entity) {
    // Throw exception if e is null or invalid entity
}

function processEntity(e?: Entity) {
    validateEntity(e);    // If it passes this line, e is not null!
    let a = e.name;  // TS ERROR: e may be null.
    let b = e!.name;  // OKAY. We are asserting that e is non-null.
}

Examples

Implicit Type detection Error

var foo = 123;
foo = '456'; // Error: cannot assign a `string` to a `number`

Misc Tips

  • set re=0 # set regexpengine = 0 to disable vim from hanging on tsx file editing.