Deepu K Sasidharan Typescript Presentation

Disclaimer: this is in no way backed by Deepu, just my notes on his talk.

The rise of MV* frameworks

Every day you have a new framework: "yet another framework syndrome". This makes it very difficult to keep up with the trends.

Javascript has the biggest ecosystem.

What is TypeScript

It is a superset of Javascript. Anything valid in Javascript is valid in Typescript and supports the latest features of Javascript (now event to ES10). Typescript is optional.

It is maintained by Microsoft, and there was a big effort by Google as well in the beginning and collaboration is still ongoing.

It comes with a great editor integration.

Transpiling is a pain

So if we have to do it, it is much better to do it with TypeScript, rather than Babel. Because if I'm going to loose that much time transpiling, I might as well have all these for free:

  • Type safety
  • Great IDE Support
  • Easier Refactoring
  • Better linting and error identification
  • Features from the future
  • Much better code style in React and Angular

Feators of TypeScript

  • Static type checking, Type inferences, Type aliasing
  • Union, intersection, function and hybrid types
  • Generics, Decorators (aka Annotations), Mixins (aka Traits)
  • Interface, Enum, Tuple support
  • Modifiers: Private, Optional, Readonly properties
  • JSX support, JS typechecking and more much

Install TypeScript

npm i -g typescript

inits a typescript project with tsconfig.json properties

tsc -init

start watching our project

tsc -w

IMPORTANT: if you set allowJs (allow js files to be compiled) or checkJs (report errors in .js files) and do not specify the outDir in your tsconfig.json, then your .js files will be overridden.

In strict mode, types like string do not accept null as a value and you need to do string | null.

Interfaces

interface Person {
    name: string;
    surname: string;
    age: number;
    sayHi: function;
}

If we compile the interface as is, the above does not produce any code in the js output, it is used only at the TypeScript level and will check the type of for example:

let john: Person = {
    name: "John",
    surname: "Di Falco",
    age: 12,
    sayHi: function() {
        console.log(`Hi ${this.name}`);
    },
    bob: "Hey", // [ts]: Error Object literal may only specify known properties, and 'bob' does not exist in type 'Person'
}

let frank: Person = {}; // [ts]: Error Property 'name', 'surname' ... are missing in type '{}' but required in type 'Person'

So interfaces are TypeScript contracts, restricting to exactly what is specified in the interface.

Access properties: allow you to declare unknown property names and associate them to some type. "You are allowed to add properties of which I do not know the name, as long as their value is of some type".

interface Person {
    name: string;
    [key: string]: number;
}

Here we allow unknown name properties

Moreover, when you refactor the interface by changing a property, it will automagically rename all objects of that interface type.

You can also have readonly properties in interfaces. Meaning that they can't be changed after creation.

interface Person {
    readonly name: string;
    age: number;
    readonly sayHi: function;
}

Function interfaces

interface AddFn {
    (a: number, b: number): number;
}

const add: AddFn = (a, b) => {
    return a + b;
}

Extending interfaces

interface Person {
    readonly name: string;
}

interface SuperHero extends Person {
    power: string;
    applyPower: () => void;
}

It has the effect of adding more properties that require to be implemented AND initialized inside the construtor if not using the shortcut.

class JohnnyBravo implements SuperHero {
    readonly name: string;
    power: string;

    constructor(name: string, power: string) {
        this.name = name;
        this.power = power;
    }

    applyPower() {
        console.log(`Hoo yeah, where's me comb!?`);
    }
}

Constructor initializer shortcut

class JohnnyBravo implements SuperHero { 
    constructor(readonly name: string, power: string) {}
    // constructor has no body, automatically assigns

    applyPower() {
        console.log(`Hoo yeah, where's me comb!?`);
    }
}

Extending classes to turn them into interfaces

interface KoolKid extends JohnnyBravo {
    hooYaa: function;
}

Class property Modifiers (private, protected, public, static)

class FatJohnny extends JohnnyBravo { 
    static unit: string = "kg";
    private weight: number;

    // cannot use constructor shortcut for subclasses
    constructor(name: string, power: string, weight: number) {
        super(name, string); // this step is crucial
        this.weight = weight
    }

    protected gun() {
        console.log('can be called by subclasses');
    }
}

Abstract classes

abstract class Haha {
  constructor(bell: string) {}
  foo(): string {
    return "hey";
  }
}

let ha = new Haha("asdf"); // [ts] Cannot create an instance of an abstract class

class Hehe extends Haha {}

let hehe = new Hehe("asdf"); // ok

class Hahaha extends Haha {
  bar() {
    console.log('bar tender');
  }
}

let haha: Haha = new Hahaha("asdf"); // [ts] type Haha
haha.bar(); // [ts] Error: property 'bar' does not exist on type Haha
let hahaha = new Hahaha("asdf"); // [ts] type Hahaha
hahaha.foo(); //ok
hahaha.bar(); //ok

Function overloading

function add(x: string, y: string): string;
function add(x: number, y: number): number;
function add(x: any, y: any): any {
    if (typeof x === 'string') {
        return `${x} + ${y} = ${add(parseInt(x), parseInt(y))}`;
    } else if (typeof x === 'number') {
        return x + y
    }
    throw new Error('Never reached but not detected by ts');
}

add("1", "2");
add([], []); // [ts] Error! No overload matches this call.
//  Overload 1 of 2, '(x: string, y: string): string', gave the following error.
//    Argument of type 'never[]' is not assignable to parameter of type 'string'.
//  Overload 2 of 2, '(x: number, y: number): number', gave the following error.
//    Argument of type 'never[]' is not assignable to parameter of type 'number'.

The throw Error above is never thrown because the function definition is not part of the overlaod. Meaning that it can only be called with two sets of paramters: string or number.

Generics

class Greeter<T> {
    private greeting: T;
    constructor(message: T) {
        this.greeting = message;
    }

    public greet() {
        return `Hello ${this.greeting}`;
    }
}
const greeter = new Greeter<string>("World");
const greeter2 = new Greeter<number>(10);

Generics restriction (narrowing)

We can also use generics but restrict them to some type:

interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); // ok to use that since T extends Lengthwise
    return arg;
}
const lw = {
    randomStuff: 2,
    length: 1,
}

const nolen = {
  randomStuff: 2,
}
loggingIdentity<typeof lw>(lw);
loggingIdentity<typeof lw>(nolen); // [ts] Error: Argument of type '{ randomStuff: number; }' is not assignable to parameter of type '{ randomStuff: number; length: number; }'.
loggingIdentity<typeof nolen>(nolen); // [ts] Error: Type '{ randomStuff: number; }' does not satisfy the constraint 'Lengthwise'.

Dead code removal

Tyescript removes code that is unreachable when it compiles js.

Namespaces

Namespaces allow you to modularize in a sort of sandbox your code. Useful for library writers etc.

namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }

    const lettersRegexp = /^[A-Za-z]+$/;
    const numberRegexp = /^[0-9]+$/;

    export class lettersOnlyValidator implements StringValidator {
        isAcceptable(s: string): boolean {
            return lettersRegexp.test(s);
        }
    }

    export class ZipValidator implements StringValidator {
        isAcceptable(s: string): boolean {
            return numberRegexp.test(s);
        }
    }
}

const validator = new Validation.lettersOnlyValidator()
validator.isAcceptable("asdf0");

Decorators

You can decorate:

  • Methods
  • Classes
  • Properties
  • Paramters
  • Accessor

[Decorators][decorators-ts] are considered experimental even though they have been there for a while. To enable them you need to "experimentalDecorators": true in your tsconfig.json:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

Note: property descriptors are a Javascript thing with the following API:

const propertyDescriptor = {
    configurable: false, // true <=> type of this prop descr. may be changed and if the prop may be deleted from the corresponding object
    enumerable: false, // true <=> property shows up during enumeration of properties on the corresponding object

    // only for data descriptors optional keys
    value: undefined, // value associated with the property. Can be any valid javascript
    writable: false, // true <=> associated with the property may be changed with an assignement operator

    // accessor descriptors also have these optional keys
    get: undefined, // a function which serves as a getter for the property, when the property is accessed this function is called with no arguements.
    set: undefined, // a function chich serves as a setter for the property
}

// using the descriptor on an empty object to define a key "myKey"
let obj = {};
Object.defineProperty(obj, 'myKey', propertyDescriptor);
obj.myKey;

Here you see that there is a descriptor as third parameter, which is passed to the decorator with the decorated prop's descriptor.

// direct decorator
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function() {
        console.log(`decorated ${propertyKey}, the method code is: `, originalMethod);
        originalMethod();
    }
}

//decorator factory
function logFactory(someParam: string) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const originalMethod = descriptor.value;
        descriptor.value = function(...args) {
            console.log(`decorated ${propertyKey}, the method code is: `, originalMethod, someParam);
            originalMethod.apply(this, args);
        }
    }
}

class Foo {
    @log
    foo() {
        console.log("Hello there!");
    }

    @logFactory("a config")
    boo() {

    }
}

const p = new Foo();
p.foo();

Decorators basically transpile to javascript code that uses Reflection a lot. It is an API that allows inspection.

IMPORTANT: if the decorator method returns a value, it is used as the Property Descriptor.

IMPORTANT: decorators can be composed like in maths

function f() {
    console.log("f()")
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        let method = descriptor.value;
        descriptor.value = function(...args) {
            console.log("f:addition called\n");
            method.apply(this, args)
        }
    }
}

function g() {
    console.log("g()")
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        let method = descriptor.value;
        descriptor.value = function(...args) {
            console.log("g:addition called\n");
            method.apply(this, args)
        }
    }
}

class C {
    @f()
    @g()
    foo() {
        console.log("foo called");
    }
}

let c = new C;
c.foo();
// f()
// g()
// g:addition called
// f:addition called
// foo called

See the order of calling is like in maths. Using the decorator factory makes initial factory calls which return the decorators themselves. Then the actual decorators get called, composing from the last the first: f o g o foo.

Decorator Factory

Another example using the decorator factory in order to toggle the desired decorator behavior:

class Greeter {
    constructor(greeting: string) {}

    @enumerable(false)
    greet() {
        return `Hello ${this.greeting}`;
    }
}

function enumerable(value: boolean) {
    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    }
}

Class Decorators

The class decorator is declared just before the class declaration.

The class decorator is applied to the constructor of the class and can be used to observe, modify or replace a class definition. A class decorator cannot be used in declaration file, or in any other ambient context (such as on a declare class).

The expression for the class decorator will be called as a function at runtime, with the constructor of the decorated class as its only argument.

If the class decorator returns a value, it will replace the class declaration with the provided constructor function.

NOTE: SHould you choose to return a new constructor function, you must take care to maintain the original prototype. The logic that applies decorators at runtime will not do this for you.

Let's for example seal an object, preventing new properties from being added to it and making all existing properties non-configurable.

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return `Hello ${this.greeting}!`;
    }
}

function sealed(constructor: Fuction) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

This effectively seals the constructor and it's prototype. Meaning that we cannot add anything or edit anything in the class.

IMPORTANT: take good notice on how we need to Object.seal the prototype as well.

How to override the constructor:

function classDecorator<T extends {new(...args: any[]):()}>(constructor: T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}

@classDecorator
class Greeter {
    property = "property";
    hello: string:
    constructor(m: string) {
        this.hello = m;
    }
}

console.log(new Greeter("world"))

Accessor Decorator

Note: typescript disalows decorators on both get and set accessors. This is because the decorators are applied to the Property Descriptor and it contains both the get and set.

class Point {
    private _x: number;
    constructor(x: number) {
        this._x = x;
    }

    @configurable(false)
    get x() { return this._x; }
}

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    }
}

Property & Paramter decorator

[Read this post][reflection-metadata]. There is a reflection metadata library for typscript that can be installed using.

npm i reflect-metadata

Constructor Type

class Animal {}
class Penguin extends Animal {}
class Zebra extends Animal {
    a: string;
    constructor(a: string) {
        super();
        this.a = a;
    }
}

class Zoo<T extends Animal> {
    constructor(public readonly AnimalClass: new() => T) {}
}

// or different notation
class Zooo<T extends Animal> {
    readonly AnimalClass: new() => T;
    constructor(animalClass: new() => T) {
        this.AnimalClass = animalClass;
    }
}

// or allowing a constructor with many arguments 
class Zooo<T extends Animal> {
    constructor(public readonly AnimalClass: new(...args: any[]) => T) {}
}

const penguinZoo = new Zoo(Penguin);
const zooPenguin = new penguinZoo.AnimalClass()

const zebraZoo = new Zooo(Zebra);
const zooZebra = new zebraZoo.AnimalClass("zebrilla");
zooZebra.a

Intersection and Union types

An intersection type combines multiple types into one: Person & Wolf. Only values that conform the union of properties. Think of it as different light colors adding up and intersecting creating a white light. So any light having that white color will match the type intersection.

interface Runner {
    speed: number;
    run(distance: number): number;
}
interface Swimmer {
    speed: number;
    swim(distance: number): number;
}

const p1: Runner & Swimmer = {
  speed: 0,
  swim: (a: number) => a,
  run: (a: number) => a,
}

If you try to create an intersection from two primitive types, you will end up with type never:

type myNeverType = number & string; // never

Since there is no value that will ever be number and string, the type formed by the intersection above is actually never.

A union type allows you to have either one or the other or both: string | number. This is very useful for allowing different types in a parameter for example.

const n1: Runner | Swimmer = { // ok
  speed: 1,
  swim: (a: number) => a,
};

const n2: Runner | Swimmer = { // ok
  speed: 1,
  run: (a: number) => a,
};

const n3: Runner | Swimmer = { // ok
  speed: 1,
  run: (a: number) => a,
  swim: (a: number) => a,
};

const n4: Runner | Swimmer = { // [ts] Error: Type '{ speed: number; }' is not assignable to type 'Runner | Swimmer', Property 'run' is missing
  speed: 1,
};

But it is important to note that on a value that is of a union type, you can only access members that are common to each of the inner types in the union.

n3.swim; // [ts] ok
n3.run(10); // [ts] Error, run is  not guaranteed to be available
n2.swim(10) // [ts] Error : [js] Access of undefined

Note that in the example above, n3 should be defined as an intersection rather than a union, since we implement all properties. This would allow us to call n3.run(10) or n3.swim(10)

Type Guads

Union types are useful to model situations where values can be of types several types. But when these types do not share any member, we cannot access any member of the union type value. How then can we tell which of the types within the union, a union type value has? For example if we have the union type: Dog | Tortoise | Fish how can we tell if we have a Fish specifically? Using type guards.

First see an example of failing code without any check:

const n1: Runner | Swimmer = { // ok
  speed: 1,
  swim: (a: number) => a,
};

if (n1.swim) { // [ts] Error
    n1.swim(10);
} else if (n1.run) { // [ts] Error
    n1.run(10);
}

To still be able to check the presence of a specific member that is not shared by all the subtypes in the union type, we use type assertions like this:

if ((n1 as Swimmer).swim) { // ok
    (n1 as Swimmer).swim(10);
} else if ((n1 as Runner).run) { // ok
    (n1 as Swimmer).run(10);
}

User-defined Type Guards

There is much repetition in the code above. Is there a way to test once and assume afterwards? Instead of type assertions we need to use type guards.

Type Guard: some expression that performs runtime check that guarantees the type in some scope.

Type Guards with Type Predicates

To define a type guard, we simply need to define a function whose return type is a type predicate: paramName is Type like in the follwing example:

function isRunner(athlete: Runner | Swimmer): athlete is Runner {
    return (athlete as Runner).run !== undefined;
}

const myAthlete: Runner | Swimmer = { // ok
  speed: 1,
  swim: (a: number) => a,
};

// BOTH calls to 'run' and 'swim' are now ok

if (isRunner(myAthlete)) {
    myAthlete.run(10);
} else { // if we dont have a Runner it must be a Swimmer
    myAthlete.swim(10);
}

the athelete is Runner is our type predicate in the example above.

Type Guards with the in operator

The in operator now acts as a narrowing expression for types.

For a n in x expression, where n is a string literal or string literal type and x is a union type, the "true" branch narrows to types which have an optional or required property n, and the "false" branch narrows to types which have an optional or missing property n.

function move(athlete: Runner | Swimmer) {
    if ("swim" in athlete) {
        return athlete.swim(10);
    }
    return athlete.run(10)

}

Type Guards with typeof

When you are dealing with unions of primitive types, it is kind of a pain to define type predicate functions for them. Especially when in javascript we could use typeof x === 'number'.

Luckily, typescript will recognise typeof x === 'typename' as a type guard on its own (typename must be one of: number, string, boolean, or symbol). So this would be allowed as well:

const a2: string | number = (function (): string | number {
    return (0.5 > Math.random())? "" : 1;
})();
if (typeof a2 === 'number') {
  a2.toFixed();
} else {
  a2.toLowerCase();
}

Type Guards with instanceof

Similar to how typeof works on primitive types, instanceof narrows down types using their constructor function.

if (hey instanceof SomeHeyClass) {
    hey.someHeyMethod(); // type of hey narrowed to SomeHeyClass
}

The right hand side of instanceof needs to be a constructor function, and typescript will narrow it down to:

  1. the type of the function's prototype property if it is not any
  2. the union of types returned by that type's construct signatures

Nullable types

TypeScript has two special types, null and undefined, that have the value null and undefined respectively. By default, the type checker considers null and undefined assignable to anything (so they are valid values for every type). This means it is not possible to stop them from being assigned to any type: "billion dollar mistake".

--strictNullChecks flag fixes this; when you declare a variable, it doesn;t automatically include null or undefined. But then you can include them explicitely with a union type:

let sn: string | null = "bar"
sn = null; // [ts] ok

Optional parameter and properties

With --strictNullChecks, optional parameters and properties automatically get | undefined added to the specified type:

const a = function same(b?: string): string {
    return b || "":
}

// without strict null checks
same(null); // [ts] ok
// with strict null checks
same(null); // [ts] Error - null is not assignable to string | undefined

or with properties

class C {
    a?: number;
}
let c = new C();
// with strict null checks
c.a = null; // [ts] Error - null is not assignable to string | undefined

Removing null or undefined

We can remove null with if and else branches testing for null or with the terser operation:

function hey(name: string | null): string {
    return name || "Bob";
}

function ho(name: string | null): string {
    if (name == null) {
        return "default";
    } else {
        return name;
    }
}

Postfix !

When you know that the value will not be null or undefined but the compiler does not allow you to make that assumption because of the context (for example a nested function), then you can use someVar! the ! tells typescript that someVar is not going to be null or undefined.

So if someVar was of type string | null, then it will allow you to call someVar!.charAt(0) because since it is not null, it must be string.

let a: string | null;
a = null;
a.charAt(0); // [ts] Error - Object is possibly 'null'
a!.charAt(0); // [ts] Ok ; [js] - Error Uncaught TypeError: Cannot read property 'charAt' of null

But of course you would not want to do that. The proper thing here is to rely on typescript's error, and pass a string to a instead of null. Otherwise crashes in js.

But there are times when typescript does not know what the actual value of a variable is at compile time. Even if you have taken precautions (1) to make sure it is never null like in the following example:

function broken(name: string | null): string {
    function nested(): string {
        return name.charAt(0); // [ts] Error - Object is possibly 'null'
    }
    name = name || "Bob"; // (1) Make sure name is always string
    return nested();
}

In the above example, TypeScript is incapable of knowing the value of name within the nested function nested, even though we set it to "Bob" when null in (1).

IMPORTANT: since at this point we know better what the value of name will be withing the nested function because of the precation (1), we can tell typescript to remove the null type from the name variable with the postfix !.

function solved(name: string | null): string {
    function nested(): string {
        return name!.charAt(0); // [ts] - Ok, we tell ts that name does not contain null, so it contains only string
    }
    name = name || "Bob";
    return nested();
}

Note: it has nothing to do with the fact that we took the precaution in (1) that makes typescript allow us to use !. The usage of ! is allowed anywhere and it will make typescript remove the possible null value from the var, regardless of whether it is null or not. So we are smarting out typescript.

IMPORTANT: whenever you write a union type, also write a typeguard

Optional Chaning : Postfix ?.

Optional property access lets you tell TypeScript to access a property unless it is null or undefined, in which case TypeScript should stop and return undefined.

let x = foo?.bar.baz();

Above if foo === undefined || foo === null then x will be undefined otherwise it will receive foo.bar.baz(). It's equivalent to:

let x = (foo === undefined || foo === null)
    ? undefined
    : foo.bar.baz();

And to check if baz is also present we would do:

let x = foo?.bar?.baz.?();

// replaces
let x;
if (foo && fo.bar && foo.bar.baz) {
    x = foo.bar.baz();
}

Keep in mind that ?. acts differently than && since && will exit for any falsey value like 0, NaN, "" and false. It is an intentional feature, which should be used to test whether the property "exists".

Optional element access acts similarly to optional property access, but it allows us to access non-identifier properties (e.g. arbitrary strings, numbers, and symbols):

// get the first element of an array if we have an array,
// otherwise return undefined
function tryGetFirstElement<T>(arr?: T[]) {
    return arr?.[0];
    // equivalent to
    // return (arr === null || arr === undefined)? undeinfed: arr[0]
}

Note: In javascript accessing a nonexistent key of an array returns undefined but no error. So that is why we do not test for the length of the array.

Optional Call allows us to conditionally call expressions if they're not noll or undefined.

async function makeRequest(url: string, log?: (msg: string) => void) {
    log?.(`Request started at ${new Date().toISOString()}`);
}

Important: the ?. magic stops at property access level, so if it is part of a larger expression, the rest of the expression will still be executed and have to deal with an undefined in case of null or undefined.

let res = foo?.bar / someComputation();
// is equivalent to
let temp = (foo === undefined || foo === null)

Nullish Coalescing: ??

The nullish coalescing operator : ?? is a way to "fall back" to a default value when dealing with null or undefined:

let x = foo ?? bar();
// equivalent to

let x = (foo !== null && foo !== undefined)
    ? foo
    : bar();

It can replace uses of ||, which will disregard falsey values, whereas ?? will evaluate to the right hand side part only with null or undefined.

let volume = localStorage.volume || 0.5; // if ls.volume is 0, volume will be set to 0.5
// with
let volume = localStorage.volumen ?? 0.5; // if ls.volume is 0, volume will be set to 0
  • The right argument of ?? is evaluated only if needed ("short circuiting").
  • ?? has lower precedence than ||.
  • ?? cannot immediately contain, or be contained within, an && or || operation.
  • The right argument is selected if the left argument is null or undefined.

Type Aliases

Type aliases create a new name for a type. Type aliases are almost like interfaces. Whaever can defined with an interface you can also defined as a type. The only difference, is that with type, you can also alias primitives, and tuples:

type GreeterAlias = Greeter;
type StringTuple2 = [string, string];

Type aliases are nice to keep your code clean, for example instead of writing:

function (myAnimal: (Bird & Reptile) | Insect): (Bird & Reptile) | Insect {
    // some code herec
}

you can write:

// Bird and reptile intersection (both at the same time)
type Dragon = Bird & Reptile;
// Dragon and Insect union (either is ok)
type DangerousAnimal = Dragon | Insect;

// In a sigle statement
// type DangerousAnimal = (Bird & Reptile) | Insect;

function (myAnimal: DangerousAnimal): DangerousAnimal {
    // some code here
}

Just like interfaces, aliases can also be generic - we can add type paramters like so:

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>;
    right: Tree<T>;
}

Together with intersection types we can create some pretty mind blowing types:

type LinkedList<T> = T & { next: LinkedList<T> | null };

interface Person {
    name: string;
}

let people: LinkedList<Person>;
people = { name: "John", next: null };
people.next = people;
people.next.next.next.next.name; // [js] "John"
// or
people = { name: "John", next: null };
people.next = { ...people };
people.next.name; // [js] "John"

Interfaces vs. Type Aliases

Type aliases act sort of like interfaces except in some subtle cases:

One main difference is that interfaces create a new name. Type aliases don't create a new name - for instance, error messages won't use the alias name. Hovering over aliased, will show an object literal as return type, whereas interfaced will show Interface as return type (not the case when I tested).

type Alias = { num: number };
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;

Important: an ideal property of software is being open to extension, you should always use an intefface over a type alias if possible. On the other hand if you cannot express some shape with an interface and you need to use a union or tuple type, then type aliases are usually the way to go.

String Literal Types

String literal types allow you to specify the exact value a string must have. In practice, string literal types combine nicely with union types, type guards, and type aliases.

You can use these features together to get enum-like behavior with strings:

type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIELement {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {

        } else if (easing === "ease-out") {

        } else if (easing === "ease-in-out") {

        } else {
            // Code never goes here if strictNullChecks is on, but since we did not use a switch statement on the easing param; TypeScript is not aware of that the code will never go here, so it does not complain.
        }
    }
}

This is really handy to get some intellisense on specific api's and make sure that you only pass certain values to it.

Note: The above code can be rewritten such that typescript complains if not all the cases are handled in the implementation, one is using a switch and a return type for animate, the other option is to use the assertNaver pattern like this:

function assertNaver(x: never): never {
    throw new Error("Unexpected object: " + x);
}
class UIELement1 {
    animate(dx: number, dy: number, easing: Easing) {
        if (easing === "ease-in") {
            return "";
        } else if (easing === "ease-out") {
            return "";
        } else {
            return assertNaver(easing); // [ts] Error Argument of type '"ease-in-out"' is not assignable to parameter of type 'never'
            //                 ~~~~~~
        }
    }
}

String literal types can also be used to distinguish overloads:

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overlaods
function createElement(tagName: string): Element {
    // ... some code
}

Numeric Literal Types

type DiceOutcome = 1 | 2 | 3 | 4 | 5 | 6;
function rollDice(): DiceOutcome {
    return Math.ceil(Math.random() * 6) as DiceOutcome;
}

They are seldom written explicitely, but they can be useful narrowing issues and catch bugs.

function foo(x: number) {
    if (x !== 1 || x !== 2) { // [ts] Error Operator !== cannot be applied to types '1' and '2'
        //         ~~~~~~~
    }
}

Above after the first test of inequality, we knwow that x === 1, therefore TypeScript is telling us that the second part || x !== 2 is useless (and a bug).

Enum Member Types

When all members in an enum have literal enum values, some special semantics come into play:

  1. enum members become types and we can say that certain members can only have the value of an enum member:

    enum ShapeKind {
        Circle,
        Square,
    }
    interface Circle {
        kind: ShapeKind.Circle;
        radius: number;
    }
    
    let c: Circle = {
        kind: ShapeKid.Square, // [ts] Error: Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'
        radius: 100,
    }
    
  2. Enum types themselves effectively become a union of each enum member. Meaning that the type system is able to leverage the fact that it knows the exact set of values that exist in the enum itself. Thanks to that, Typescript can catch silly bugs where we might be comparing values incorrectly:

    function f(shape: ShapeKind) {
        if (shape !== ShapeKind.Circle || shape !== ShapeKind.Square) {
            // [ts] Error Operator !== cannot be applied to types 'ShapeKind.Circle' and 'ShapeKind.Square'
    
        }
    }
    

    This one is the same as the test with 1 !== x || 2 !== x, typescript knows that when x is 1 it will always be different than 2. Similarly it knows that ShapeKind.Circle will allways be different than ShapeKind.Square since they are different members of the same enum.

Discriminated Unions

Using a combination of singleton types, union types, type guards, and type aliases we can build an advanced pattern called Discriminated Unions aka tagged unions or algebraic data types. Discriminated unions are useful in functional programming.

  1. Types that have a common, singleton type property - the discriminant
  2. A type alias that takes the union of those types - the union
  3. Type guards on the common property
interface Square {
    kind: "square"; //discriminant
    size: number;
}
interface Rectangle {
    kind: "rectangle"; //discriminant
    width: number;
    height: number;
}
interface Circle {
    kind: "circle"; //discriminant
    radius: number;
}

// now we union them into a common theme
type Shape = Square | Rectangle | Circle; // all have kind prop which is called the discriminant

// and we use the discriminated union
function area(s: Shape) {
    switch(s.kind) { // we discriminate using the discriminant prop
        case "sqaure": return s.size ** 2;
        case "rectangle": return s.height * s.width;
        case "circle": return 2 * Math.PI *s.radius ** 2;
    }
}

Polymorphic this

A polymorphic this type represents a type that is the subtype of the containing class or interface. This is called F-bounded polymorphism. This makes hierarchi fluent interfaces much easier to express.

class BasicCalc {
    constructor(protected value: number = 0) {}
    add(x: number): this {
        this.value += x;
        return this;
    }
    ans() {
        return this.value;
    }
}

class ScientificCalc extends BasicCalc {
    sin(): this {
        Math.sin(this.value);
        return this;
    }
}

let v = new ScientificCalc()
    .add(10)
    .sin();

Since the class uses this types, you can extend it and the new class can use the old methods with no changes.

Note: when should super be used in a constructor? Whenever we extend the superclass, and the extending class overrides some method (notable the constructor), then need to call super(params) within the extending's class constructor. Similarly for methods we want to override (and reuse the parent behavior).

Index Types

With index types, the compiler can check code that uses dynamic property names. For example a common pattern is to pick a subset of properties from an object:

function pluck(o, propNames) {
    return propNames.map(k => o[k]);
}

In order to implement this in TypeScript we need to use Index type Query and Indexed access operators:

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

interface Car {
    manufacturer: string;
    model: string;
    year: number;
}
let taxi: Car = {
    manucaturer: "Toyota",
    model: "Prius",
    year: 2014,
};

let makeAndModel: string[] = pluck(taxi, ["manufacturer", "model"]);
let modelYear: (string | number)[] = pluck(taxi, ["model", "year"]);

The compiler checks that manufacturer and model are actually properties on Car. The example introduces a couple of new type operators:

  1. keyof T is the index type query operator. For any type T, keyof T is the union of known, public property names of T.

    let carProps: keyof Car; // the union of 'manufacturer' | 'model' | 'year'
    
  2. T[K] is the indexed access operator. Here the type syntax reflects the expression syntax: Car['model'] has the type Car['model'] which here is string. However just like index type queries, you can use T[K] in a generic context, which is where its real power comes to life. You have to make sure that the type variableK does extends the type keyof T.

keyof T

Index type query operator allows you to create a string literal type union of the known public property names of T :

interface Car {
    model: string;
    manufacturer: string;
    year: number;
}

let carProp0: 'model' | 'manufacturer' | 'year' = 'model'
// equivalently
let carProp1: keyof Car = 'manufacturer';
// equivalently
type CarProps = keyof Car;
let carProp2: CarProps = 'year';

function f(a: typeof carProp1): typeof carProp2 {
    return a;
}

function g(a: keyof Car): keyof Car {
    return a;
}

function h(a: CarProps): CarProps {
    return a;
}

let carProp3: [keyof Car, CarProps, typeof carProp0] = [f('model'), g('model'), h('model')]; // [ts] Error - Type '"model" | "manufacturer" | "year"' is not assignable to type '"manufacturer"'

The problem above comes from the fact that even though keyof Car, CarProps and typeof carProp0 produce the same type : 'manufacturer' | 'model' | 'year', when getting the type from a variable with typeof carProps0 only in the context of a tuple type definition, here: [typeof Car, CarProps, typeof carProp0], TypeScript limits the type in the 3rd position to the value of carProp0 which is only 'model'.

Indexed Access Operator: T[K]

Indexed access operator the type syntax T[K] reflects the type of the expression syntax car['model'], which in our following example is string.

function getProp<T, K extends keyof T>(o: T, propName: K): T[K] {
    return o[propName];
}

let car: Car = new Car();
let prop0 = getProp(car, 'year'); // [ts] ok prop is of type 'number'
let prop1 = getProp(car, 'manufacturer'); // [ts] ok prop is of type 'string'

So T[K] type syntax is refering to: the type of the property K belonging to T. And TypeScript is capable of telling what the actual return type will be depending on the paramters provided to getProp.

Below, is an example of an array of such object properties. The array is of type: T[K][]:

function getProps<T, K extends keyof T>(o: T, propNames: K[]): T[K][] {
    return propNames.map(prop => o[prop];
}

let car: Car = new Car();
let props = getProps(car, ['year', 'manufacturer']); // [ts] ok props is of type 'number' | 'string'

You see above, that the array props (containing the selected props of object car) is of type array of type union of each of the selected props type, here: ('number' | 'string')[].

But look at this modification:

// ... getProps method as before

let car: Car = new Car();
let selection = ['year', 'manufacturer'];
let props = getProps(car, selection); // [ts] Error Argument of type 'string[]' is not assignable to parameter of type '("year" | "model" | "manufacturer")[]'

Above, the selection variable is of type string[] therefor it is too generic to use it as second parameter which requires an array of only the union of the string literal types: 'year', 'model' or 'manufacturer'. We can solve this by using keyof operator:

// ... as before 
let selection: (keyof Car)[] = ['year', 'manufacturer'];
let props = getProps(car, selection); // [ts] ok - props is of type ('string' | 'number')[]

Index Signatures

In JavaScript, an object can be accessed with a string to hold a reference to any other JavaScript object:

const obj = {};
obj["myprop"] = "some value";
console.log(obj["myprop"]); // "Some other object"

We store the object "some value" using the string "myprop".

But if you pass any other object than a string as accessor, JavaScript's runtime will call .toString on the accessor object.

const obj = {};
const notAString = { hey: "you", };

obj[notAString] = "some value";
console.log(obj[notAString]); // [js] "Some other object"

// but
console.log(obj[{ a: "different object", }]); // [js] "Some other object"

What happened above, is that JavaScript is calling .toString on an instance of Object, which returns the string "[object Objec]" (if not explicitely overriden). So when using any instance of Object as property accessor, JavaScript will access the key "[object Object]" which is a valid property since it is a string.

We can circumvent this problem by overriding the default Object.toString method inherited by any Object instance.

const obj = {};
const accessorObj = {
    a: "my stuff",
    toString() {
        return a;
    }
};

obj[accessorObj] = "some value";
obj[{}] = "another value"

console.log(obj[accessorObj]); // [js] "some value"
console.log(obj[accessorObj.toString()]); // [js] "some value"
console.log(obj[{}]); // [js] "another value"
console.log(obj["object Object"]); // [js] "another value"

We have been able to change where "some value" is stored by overriding the default Object.toString method in accessorObj. And when using another object {} we sotre it under "[object Object]".

Note: Arrays are a bit different. For number, indexing JavaScript VM's will try to optimize, so number should be considered a valid object accessor in its own right (distinct from string).

let foo = ["World"];
console.log(foo[0]); // [js] "World"

IMPORTANT: If we are not carefull, it is easy to inadvertently store a value under the wrong property when relying on JavaScript's default feature of calling .toString under the hood for us. That is why TypeScript forces us to explicitely call .toString on accessor objects that are not of type strings or number (Symbol too).

In TypeScript we need to do the following:

const obj: any = {};

const accessorObj = {
    a: "my stuff",
    toString(): string {
        return a;
    }
};

obj[accessorObj] = "some value"; // [ts] Error - the index signature must be string

// FIX
obj[accessorObj.toString()] = "some value"; // [ts] Ok

console.log(obj[accessorObj.toString()]); // [ts] "some value"

You are not forced to explicitely call .toString for string and number types, because out of the box, number has a nice toString implementation that returns the number in string representation.

Declaring index signatures

TypeScript will allow us to set an index (when an Index Signature has not been defined priorly) only if we explicitely set the object type to any:

let stringIndexObject = {};
stringIndexObject["hello"] = "you"; // [ts] Error = Element implicitly has an 'any' type because expression of type '"hello"' can't be used to index type '{}'. Property 'hello' does not exist on type '{}'.

// FIX
let anyObject: any = {};
anyObject["hello"] = "you"; // [ts] Ok
anyObject[1] = "you"; // [ts] Ok

Using any is against TypeScripts philosophy, so a better fix would be to be more explicit using index signatures:

let numberIndexObject: { [index: number]: string, };
let numberIndexObject = {};

numberIndexObject[1] = "";

let stringIndexObject: { [index: string]: SomeOtherType, } = {};

stringIndexObject[(1).toString()] = "";
stringIndexObject[1] = ""; // [ts] Ok even if number for string index, relying on implicit javascript accurate runtime number conversion to string

Notice how TypsScript will prevent us from using any other index signature than the one defined in the object type (which are limited to string and object):

numberIndexObject["1"] = ""; // [ts] Error - Element implicitly has an 'any' type because index expression is not of type 'number'
numberIndexObject[(1).toString()] = ""; // [ts] Error - Element implicitly has an 'any' type because index expression is not of type 'number'

Note: You can change how you name the index signature, for TypeScript it does not affect anything. Change it to give some context, for example, if the object keys are user names, then we can define the object index signature as:

let users: { [username: string]: SomeUserData, };

IMPORTANT: as soon as you have a string index signature (like [index: string]: someType) all explicit members must also conform to that signature and return a someType. This is to provide safety so that any string access returns the same result (Wtf??? same type?).

interface Foo {
    [key: string]: number;
    x: number;
    y: string; // [ts] Error - Property 'y' of type 'string' is not assignable to string index type 'number'  
}

// but with number it is ok
interface Bar {
    [key: number]: string;
    1: string;
    x: string; // [ts] ok
    y: number; // [ts] ok
}

Using limited set of string literals

An index signature can require that index strings be mebers of a union of literal strings by using Mapped Types:

type Index = 'a' | 'b' | 'c';
type FromIndex = { [k in Index]?: number };

const good: FromIndex = { b: 1, c: 2, };

const bad: FromIndex = { x: 1, b: 1, }; // [ts] Error - Object literal may only specify known properties, and 'x' does not exist in type 'FromIndex'

Specificatino of the vocabulary can be deffered to generically:

type FromSomeIndex<K extends string> = {[key in K]: number};

Having both string and number indexes

It is a very rare use case but TypeScript suppoorts it not the less

interface ArrStyr {
    [key: string]: string | number; // Must accomodate all members

    [index: number]: string; // Can be subset of string

    length: number; // just an example number
}

Design Pattern: Nested index signature

Instead of using this:

interface NestedCss {
    color?: string;
    [selector: string]: string | NestedCSS | undefined;
}

const example: NestedCSS = {
    color: "red",
    ".subclass": {
        color: "blue"
    },
}

// a typo in `color` is not catched
const failSilently: NestedCSS = {
    colour: 'red', // [ts] ok even though there is a typo
}

To avoid these kind of situations, it is best to spearate the nesting into its own property:

interface NestedCSS {
    color?: string;
    nesting?: {
        [selector: string]: NestedCSS;
    }
}

const failsOnTypo: NestedCSS = {
    color: "red",
    nesting: {
        '.some-sub-class': {
            colour: "red", // [ts] Error - unknown property 'colour'
        }
    },
}

Excluding certain properties from the index signature

Sometimes you need to combine properties into the index signature. This is not advised, and you should use the Nested Index Signature Pattern mentionned above.

type FieldState = {
    value: string;
}
type FormState = {
    [fieldName: string]: FieldState;
    isValid: boolean; // [ts] Error - Property 'isValid' of type 'boolean' is not assignable to string index type 'FieldState'
}

This is the error that we got previously, where once you define an index signature to have an index of type string then all members must conform to that index return type (here isValid should be FieldState and not boolean).

To work around this limitation (even though it is not advised) you can use an intersection type:

type FieldState = {
    value: string;
}
type FormState = { isValid: boolean } & { [fieldName: string]: FieldState }

IMPORTANT: even though you can declare it to model existing JavaScript, TypeScript will not allow you to create an object of type FormState. You will need to have imported the object from some library using declare:

import {foo} from 'foolibrary';

declare const foo: FormState;

const isValid: boolean = foo.isValid; // [ts] ok

const bar: FormState = {
    isValid: false, // [ts] Error - Type '{ isValid: false; }' is not assignable to type 'FormState'. Type '{ isValid: false; }' is not assignable to type '{ [fieldName: string]: FieldState; }'. Property 'isValid' is incompatible with index signature.
}

Index Types and Index Signtures

keyof and T[K] interact with index signatures. An index signature parameter type must be 'string' or 'number'.

Limit explicit use of undefined

TypeScript gives you the opportunity to document your structures separately from values so instead of writing stuff like:

function foo() {
    // if something
        return {a: 1, b: 2, };
    // else
        return {a: 1, b: undefined, };
}

you should use type annotations:

function foo(): {a: number, b?: number, } {
    // if something
        return {a: 1, b: 2, };
    // else
        return {a: 1, };
}

Don't use undefined as a means of denoting validity

An awful function like this:

function toInt(str: string) {
    return str
        ? parseInt(str)
        : undefined
}

can be much better written like this:

function toInt(): { valid: boolean, int?: number, } {
    const int = parseInt(str);
    if (isNaN(int)) {
        return { valid: false, };
    } else {
        retunr { valid: true, int}
    }
}

Object Oriented vs Functional Programming with TypeScript

To do object oriented, we perform inheritance by creating a base class from which others will inherit and then extend.

class Human {
    constructor(public name) {} // this will add name as property

    sayHi() {
        return `Hello, ${this.name}`;
    }
}

const patrick = new Human('Patrick Mullot');

class SuperHuman extends Human {
    constructor(name) { // this without private, to not add it to SuperHuman
                        //but to override Human constructor
        super(name);
        this.heroName = `${name} the Hero!`;
    }

    superpower() {
        return `${this.heroName} dabooom!!`;
    }
}

const steph = new SuperHuman('Steph Curry');
console.log(steph.sayHi()); // Hello, Steph Curry

As an alternative to inheritance simple inheritance, is using composition through the mixin pattern. You decouple your properties into different objects that functions return, and then you compose them back using a final function. This is actually a way of doing multiple inheritance, which is what you wish exists, when you realize that your classification design is poor. Crockford does not advocate for this, but let's see it anyway.

const hasName = function (name) {
    return { name, };
};

const canSayHi = function (name) {
    return {
        sayHi: function () {
            return `Hello, ${name}`;
        },
    };
};

const Person = function (name) {
    return {
        ...hasName(name),
        ...canSayHi(name),
    };
};

const sally = Person('Sally');
sally.sayHi(); // Hello, Sally

In it's current form, we loose all of the class based ergonomics. But TypeScript gives us a way to use mixins in a type based format.

This is what we use to create the mixin from the classes we define after.

function applyMixins(derivedCtor: any, baseCtors: any[]) {
    baseCtors.forEach(baseCtor => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
            derivedCtor.prototype[name] = baseCtor.prototype[name];
        });
    });
}
class CanSayHi {
    name;

    sayHi() {
        return `Hello, ${this.name}`;
    }
}

class HasSuperPower {
    heroName;

    superpower() {
        return `${this.heroName} dabooom!!`
    }
}

// instead of extending, we are going to implement
class SuperHero implements CanSayHi, HasSuperPower {
    heroName;

    constructor(public name) {
        this.heroName = `${name} the Hero!`;
    }

    sayHi: () => string;
    superpower: () => string;
}

// now the magic:
applyMixins(SuperHero, [CanSayHi, HasSuperPower]);

const ts = new SuperHero('TypeScript');

When you are implementing something, you are only concerned with its interface and not its underlying code. When implementing stuff we have the additional boilerplate code of telling TypeScript what the return values of the methods are (above sayHi and superpower)

Now let's answer the fundamental question: "is a hot dog a sandwitch"?

  • If you use inheritance, then you will have to inherit some base Sandwich class, which means it is a Sandwich
  • If you use composition, then you can just pull a Sausage and a Bun which means it is not a Sandwich

Functional programming Deepu

First-class and higher-order functions

Is conveying the idea that functions a re a first class citizen, meaning that you can pass a function as an argument to another function, or return a function from another etc. Typescript supports this and makes concepts like: closures, currying and higher-order-functions easy to write.

Higher Order Function

A function is considered of higher-order-function if any of the following is true:

  • it takes one or more functions as parameters
  • it returns another function as a result
type mapFn = (it: string) => number;

// the HOF takes an array and a function as arguments
function mapForEach(arr: string[], fn: mapFn): number {
    const newArray: number[] = [];
    arr.forEach(if => {
        // we are executing the method passed
        newArray.push(fn(it));
    });
    return newArray;
}

const list = ["Orange", "Apple", "Peach"];

const out = mapForEach(list, (it: string): number => it.length); // [6, 5, 5,]

the eample above is very convoluted for a very simple case, but you get the idea

const list = ["Orange", "Apple", "Peach"];
const out = list.map(it => it.length; // [6, 5, 5]

Closures and Currying are also possible

// this is a higher order function that returns a closure
function add(x: number): (y: number) => number {
    // a function is returned here as closure
    // variable x is obtained from the outer scope
    return (y: number): number => x + y;
}

const add10 = add(10)
const add20 = add(20);

reflection-metadata decorators-ts