Md Toy Blog

Typescript

2019-10-8T22:00:32.169Z

Read [tdd vs. statically typed][tdd-v-static] to get a refresher on why statically typed languages are an advantage for large projects.

Installation

You can check whether you have the typescript compiler installed type tsc in the command line.

To install typescript you can use npm. Use the command:

npm i -g typescript

text

This will install the tsc compiler globally.

Compiling a .ts file into javascript

tsc myscript.ts

text

This will output a .js file. This is the file that can run on the browser and that you would import in an html file through the:

<script src="./myscript.js"></script>

text

You can also call the compiler passing [options][tsc-compiler-options]:

tsc myscript.ts --noImplicitAny --target "ES5" --module

text

Config strict checking

To enforce very strict checks there are 3 directives:

  1. noImplicitAny : sometimes typescript isn't able to figure out the type of a variable, in those cases it will use type any implicitly in its place. So this means you will not get the type safety there, and tooling support. If you want to get an error in these occurrences, then use this option.
  2. strictNullChecks : (dependencies may need to be updated) by default typescript assumes that null and undefined are in the domain of every type. When strictNullChecks is enabled, null and undefined get their own types called null and undefined respectively. Whenever anything is possibly null, you can use the | type union operator to tell typescript that null is also allowed there. For example, number | null tells typescript that the variable can be a number or null. Note : if you ever have a value that typescript thinks is null but you know better, you can tell typescript to shut up with:
   declare const foo: string[] | null;
   foo.length; // error - foo is possibly null (so no .length method on it)
   foo!.length; // okay - 'foo!' is always going to be type string[] (even though its declaration allows null)
  1. noImplicitThis : No implicit any for this. When you use the this keyword outside of classes, the this can be any type. Even when you attach it to the prototype, of some other object, typescript assumes this to be any type. And you can easily misspell a property of this within the function, and never notice. So noImplicitThis complains whenever you are not enforcing a this type:
   // ... Point class somewhere else with getDistance method

   // reopen the interface
   interface Point {
     distanceFromOrigin(point: Point): number;
   }
   Point.prototype.distanceFromOrigin = function(this: Point, point: Point) {
     return this.getDistance(point); // getDistance belongs to class Point defined somewhere else
   }

Mosh hamedani's type script introduction

Classes, using typescript. Private members, short hand constructors, auto assignment getters and setters

class Point {
  constructor(private _x?: number, public y?: number) {
    // no need for this.x = x etc. typescript generates that for us.
  }

  draw() {
    console.log(this._x, this.y);
  }

  // give read access
  get x() {
    return this._x;
  }

  // give write access but with conditions
  set x(value) {
    if (value < 0)
      throw new Error('Negative values are not allowed');
    this._x = value;

  }
}

let point = new Point(1, 2); // no need to type "let point: Point = new Point(1, 2);
point.draw() // we still benefit from hinting of the class Point

text

A convention to be able to define getter and/or setter methods on private members, is to prefix the private member with an underscore, and then use the name without underscore for the setter and getter method names. These methods are called respectively on obj.x = 'something' and let x = obj.x and there exists no naming collision.

Typescript has the enum type which lets you define a set of constants related to each other.

Traversy

What does typescript offer:

  1. Static type checking
  2. Class based objects
  3. Modularity
  4. ES6 features
  5. Syntax closer to java and other high level languages

Typescript types

  • String
  • Number
  • Boolean
  • Array
  • Any
  • Void
  • Null
  • Undefined
  • Unknown
  • Tuple
  • Enum
  • Generics
let numArra: number[];
let numberArray: Array<number>;
let strArray: string[];
let stringArray: Array<string>;
let boolArray: boolean[];
let boolArray: Array<boolean>;

let tuple: [string, number];
tuple = ['asdf', 1] // good

let myVoid: void = null; // works fine
let myVoid: void = undefined; // works fine too,
let myVoid: void = 1; // compilation error
let myUndef: undefined = null; // works fine too
let myNull: null = undefined; // works fine too

text

let x: number = null; // [ts] Error Type 'null' is not assignable to type 'number'
let y: number = undefined; // [ts] Error: Type 'undefined' is not assignable to type 'number'
let z: number;
console.log(z); // [ts] Error: Variable 'z' is used before being assigned

text

Functions

--noImplicitThis will warn you when this is not explicitly bound to some object and therefore tell you that this is of type any.

The this keyword has a special meaning in JS, and more so in TypeScript.

class Hello {
  hi() {
    return function() {
      return this; // Error: 'this' implicitly has type 'any' because it does not have a type annotation.
    }
  }
  ho() {
    return () => {
      return this; // Works.
    }
  }
}

const h = new Hello(); // [ts] h: Hello     ;; [js] instance of Hello
const a = h.hi();    // [ts] a: () => any
const aCalled = a(); // [ts] aCalled: any   ;; [js] global object
const b = h.ho();    // [ts] b: () => Hello
const bCalled = b(); // [ts] bCalled: Hello ;; [js] instance of Hello, same as in h

text

A quick explanation of what is happening above: in the hi() method, which is returning a function which in turn returns this. Remember, in javascript, the this keyword will refer to the pre-dot object to which the function is attached to at call time. And in case of a standalone call, to the global object. So in the hi() case, the this within the inner returned function has no ts type. On the other hand, the arrow function retains the value of this from the parent scope where it was defined. Called the lexical scope or lexical this. Which means that this does retain the Hello object, and ts knows it too.

this parameter

Contrary to javascript, typescript allows you to set this as the first parameter to any function. This will tell typescript what this is supposed to refer to.

Forbidding standalone usage of a function this: void

You can prevent usage of this in a function by setting the function's first parameter to be this: void

let h = function(this: void) {
  return this.hello; // Error : Property 'hello' does not exist on type 'void'.
}

text

Telling the compiler what this refers to

class Hello {
  hi(this: Hello) {
    return function() {
      this.ho(); // [ts] Error 'this' implicitly has type 'any' because it does not have a type annotation
      this.notHere(); // [ts] Error 'this' implicitly has type 'any' because it does not have a type annotation
      return this; // [ts] Error 'this' implicitly has type 'any' because it does not have a type annotation
    }
  }
  ho(this: Hello) {
    return () => {
      this.ho(); // [ts] Works
      this.notHere(); // [ts] Error: Property 'bee' does not exist on type 'Hello2'
      return this; // [ts] Works: 'this' has type Hello.
    }
  }
}

text

Above, the problem that ts is complaining about in function hi(this: Hello) is the same as when no type was specified because the returned function will loose the this scope anyway. On the other hand, ho(this: Hello) using the arrow function is enough to let ts know the return type, but note that without the type annotation, ts knew the return type aswell, so what is the benefit?.

this parameter in callback

You can also run into errors with this in callbacks, when you pass functions to a library that will later call them. Because the library that calls your callback will call it like a normal function, this will be undefined. With some work you can use this parameter to prevent errors with callbacks too.

  1. First the library author needs to annotate the callback type with this:
   interface UIElement {
     addClickListener(onClick: (this: void, e: Event) => void): void;
   }
  • The this: void means that addClickListener expects onClick to be a function that does not require this type.
  1. Second, annotate your calling code with this:
   class Handler {
     info: string;
     onClickBad(this: Handler, event: Event): void {
       // Wrong! Although it would compile, it will still crash at runtime
       this.info = event.message;
     }
   }
   let h = new Handler();

   // ... somewhere else in the library
   // uiElement implements UIElement interface above
   uiElement.addClickListener(h.onClickBad); // Error!
  • With the this: Handler we make it explicit that onClickBad is expecting this to be of type Handler, so that when we pass onClickBad to addClickListener TypeScript will detect the incompatibility with the interface UIElement which requires this: void.

To Fix the error we need to change the code a bit:

class Handler {
  info: string;
  onClickGood(this: void, event: Event): void {
    // Works, but we cannot use 'this' anymore, because it is expected to be void
    console.log('Good, but no "this" usage allowed anymore in here, but runs ok' + event.message);
  }
}
let h = new Handler();

// ... somewhere else in the library
// uiElement implements UIElement interface above
uiElement.addClickListener(h.onClickGood); // Works!

text

The problem now is that with the solution above, if we want to use the this keyword then we'll need to use an arrow function as a property (which does not allow overriding from subclasses, and which is copied over every object, whereas methods are stored in the prototype). Like so:

class Handler {
  info: string;
  onClickGood: (event: Event) => {
    this.info = event.message;
  }
}

text

The above works because arrow functions capture the other this, so you can always pass them to something that expects this: void.

Function types

Functions belong to the function type which typescript figures out whenever you are declaring a function. Typescript gives you the ability to specify how a specific function is typed:

  • its argument types
  • its return type.

Therefore the function type is a superset of all the function types with specific argument counts, types and return types.

function sum(a: number, b?: number): number {
  if (b == undefined) {
    return a;
  }
  return a + b;
}

function myvoid(name: string): void {
  console.log('hello ' + name);
}

text

Named function vs. Anonymous Function

function namedFunc(a, b) { a + b; };
let anonymousFunc = function(x, y) { return x + y; };

text

Typing Functions

We can explicitely specify the function return type, but in many cases TypeScript is able to figure out the return type by itself from the type of the parameters.

function add(x: number, y: number): number {
  return x + y;
}
// typescript knows that the return type is a number
function addWithImplicitReturnType(x: nubmer, y: number) {
  return x + y;
}

text

One thing is to type a function, as above: giving the function definition a few constraints on the argument counts, types and return types. And another, is to tell typescript the type of a certain variable. Two function definitions may share a same type.

function a(hello: string, name?: string) {
  return hello + name;
}
function b(hello: string, name = "Bob") {
  return hello + name;
}
// Both functions above share the type
let ab: (p: string, q?: string) => string;

text

The default value of name is not held in the type.

Note: When using default-initialized parameters, they need not be at the end of the parameter list. The only requirement is that undefined is passed in their location when the user expects to use the default. Note: The type of the default parameter is inferred from the default value.

function buildName(name = "Bernard", lastName: string) {
  return name + " " + lastName;
}
let bn: string = buildName(undefined, "Montiel"); // ok "Bernard Montiel"
let bn: string = buildName("Montiel"); // Error: too few parameters ( "Montiel" is assigned to 'name' and 'lastName' is empty when it should be )

text

Below what we are doing is storing the function on the right side ( which has types for parameters and for the return value, and is of a certain type itself ) into the variable myAdd.

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

text

Below is the right hand side of the expression above. This is the part where we actually define the function and typescript infers its type.

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

text

and what Typescript remembers of the above's function type is:

(x: number, y: number) => number;

text

Now if we want to store this function into some variable, like above in myAdd, we need to tell typescript what type is going to be store in the variable. We could do it in two ways:

  1. Let typescript figure out the type of myAdd directly from the assignment.
   let myAdd = function(x: number, y: number): number { return x + y; };
  1. Tell typescript which type is the variable myAdd going to hold, and then store the function matching that type in it. This second way is the same as the initial example, except it is performed in two steps.
   // tell Ts what type is going to be held
   let myAdd: (x: number, y: number) => number;
   // assign a value matching that type
   myAdd = function(x: number, y: number): number { return x + y; }

Note: the part after the fat arrow => indicates the return type of a function type. And it is mandatory. In case a function returns undefined or never, you would use void or never respectively:

let returnsUndefined: (message: string, value: number) => void;
let neverReturns: (foreverMessage: string) => never;

text

Rest parameters

Required and default parameters, have one thing in common: they all talk about a single parameter at time.

Sometimes you may want to:

  • talk about many parameters at a time
  • avoid specifying the exact count of parameters in advance

Note: javascript allows you to use the arguments variable within a function to access the parameter list.

In TypeScript you can use ...rest paramters like so:

function buildName(first: string, ...restOfParams: string[]): string {
  return first + restOfParams.join(" ");
}
let employeeName: (a: string, ...b: string[]) => string = buildName("John", "Mc", "Kenzy", "The", "Great");

text

Colon : vs. Fat Arrow =>

You may wonder why sometimes the return type of a function is specified using a colon : and other times with using a fat arrow =>. The reason is that using the : may be ambiguous depending on the context where it is used. For example, given a function that accepts a callback, using : to represent the return type is ambiguous and results in a syntax error, we can overcome it with the fat arrow notation or by wrapping the the callback parameter type in {} :

// ambiguous parse, syntax error
function sendString(callback: (value: string): void);

// valid, using fat arrow
function sendString(callback: (value: string) => void);

// same thing, using curly brackets
// (harder to write, harder to parse visually)
function sendString(callback: { (value: string): void; });

text

In classes it is possible to use the fat arrow notation for properties of a class that hold a function, but not for methods.

class Foo {
  // This actually a property in typescript's eyes, not a method
  someMethod: (v: string) => boolean;
}

text

In this case TypeScript makes the distinction between methods of a class and properties of a class. Functions defined as properties like above, don't need a body, but cannot be overridden or defined as methods in subclasses. In other words you couldn't do this even though the signatures are compatible:

// Error cannot do this
class Bar extends Foo {
  someMethod(v: string): boolean {
     return v.length > 0;
  }
}

text

In conclusion, due to how syntax works in TypeScript, it is always possible to use the : syntax for a function type (use {} if needed), but it is not always possible to use the => syntax.

Function Overloads

Sometimes a function will return different types depending on way it is called. From a mathematic perspective they are two separate functions with separate input sets and output mappings. So they could be separated in two. In this sense, TypeScript allows you to use overloads, which define the different function types that the function can have and thus enable type checking.

let suits = ["hearts", "clubs", "diamonds", "spades"];
function pickCard(x: any): any {
  if (typeof x === "object") {
    let pickedCard = Math.floor(Math.rand() * x.length);
    return x[pickedCard];
  } else if (typeof x === "number") {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13, };
  }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);

text

The above function's type can be narrowed down to two. Instead of the very generic any, we can use function overloads like so:

// overload for when it is supplied with a deck of cards
function pickCard(x: { suit: string; card: number; }[]): number;
// overload for when it is supplied with a deck of cards
function pickCard(x: number): { suit: string; card: number; };
// compatible type function definition (the type should be a superset of the overloads) BUT is not an overload in itself!
function pickCard(x: any): any {
  if (typeof x === "object") {
    let pickedCard = Math.floor(Math.rand() * x.length);
    return x[pickedCard];
  } else if (typeof x === "number") {
    let pickedSuit = Math.floor(x / 13);
    return { suit: suits[pickedSuit], card: x % 13, };
  }
}

text

With this change, the overloads now give us type checked calls to the pickCard function.

IMPORTANT: It looks at the overload list and, proceeding with the first overload, attempts to call the function with the provided parameters. If it finds a match, it picks this overload as the correct overload. For this reason, it’s customary to order overloads from most specific to least specific.

Note: Note that the function pickCard(x): any piece is not part of the overload list, so it only has two overloads: one that takes an object and one that takes a number. Calling pickCard with any other parameter types would cause an error.

Interfaces

One of TypeScript's core principles is that type-checking focuses on the shape that values have also known as "structural subtyping". In typescript, interfaces fill the role of naming these types, and are a powerful way of defining contracts within your code as well as contracts outside of your project.

IMPORTANT: contrary to languages like PHP, C# or Java, you don't need to explicitly implement the interface in order for the compiler to consider their signature compatible. Only the shape matters in typescript.

function showTodo(todo: {title: string, text: string, }): void {
  console.log('This is my todo: Title ' + toto.title + ', text ' + todo.text);
}

let myTodo = {title: 'asdf', text: ' hello' };
showToto(myTodo);

//A cleaner way to do this would be to create an interface
inteface Todo {
  title: string,
  text: string,
}

function showTodo(todo: Todo): void {
  console.log('This is my todo: Title ' + toto.title + ', text ' + todo.text);
}

// an even better way would be to create a class, because of the unity principle. If it has lots of connection with some other part, then it should be together

text

Classes

When comparing types that have private and protected members, we treat these types differently. For two types to be considered compatible, if one of them has a private member, then the other must have a private member that originated in the same declaration. The same applies to protected members.

interface UserInterface {
  name: string;
  email: string;
  age: number;
  payInvoice(): void;
}
class User implements {
  name: string;
  protected email: string; // only inheritants can use this
  private age: number; // adding an access modifier

  constructor(name: string, email: string, age: number) {
    this.name = name;
    this.email = email;
    this.age = number;
  }

  payInvoice(): void {
    console.log('Pay invoice');
  }
}

let user = new User('John', 'my@mail.com', 21)

text

Types

You have to think of types as sets. Sets of possible values.

Basic Types

Boolean

The most basic data type is the simple true/false boolean value:

let isDone: boolean = false;

text

Number

As in javascript all numbers in TypeScript are floating point values. These floating point numbers get the type number.

let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

text

String

let n: number = 21;
let multilineString: string = `Hello.
Some template string with number ${n + 1}
`;

text

Array

TypeScript like javascript allows you to work with arrays of values. Array types can be defined in two ways.

  1. number[] : type of the elements followed by []
   let list: number[] = [1, 2, 3];
   let otherList: string[] = ["", 1]; // error
  1. Array<number> : Array followed by the type name between <>
   let list: Array<number> = [1, 2, 3];
   let otherList: Array<string> = ["", 1]; // error

Tuple

Tuples allow you to express an array with a fixed amount of elements of some specific types:

let tup: [string, number, boolean];
tup = ["Hello", 1, true];
tup = ["Hello", 1, 0]; // Error

text

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

console.log(tup[0].substring(2)) // works
console.log(tup[1].substring(2)) // Error 'number' does not have 'substring'

text

When setting an element with an invalid index, an error is thrown :

tup[3] = ""; // Error, property 3 does not exist on type [string, number, boolean]

text

Enum

A helpful addition to the standard set of data types in JavaScript is the enum. As in C#, enum are a way of giving more frienly names to a set of numeric values.

enum Color = {Red, Green, Blue};
let c: Color = Color.Green;

text

By default enums beggin numbering their members at 0. But that can be changed by either manually setting the first value, or setting them all manually. Then you can even get the name of the index in the enum as a string

enum Color = {Red=3, Green, Blue};
let c: Color = Color.Green; // 4

enum Color = {Red=10, Green=15, Blue=20};
let c: Color = Color.Green; // 4

let colorName: string = Color[15];
console.log(colorName); // 'Green'

text

Any

We may need to describe the type of variables whose value we do not know at the time of writing the code. These values may come from dynamic content (e.g. from the user or 3rd party libraries). In this case, we want to opt out of type checking, and let the values pass through compile time checks. To do so, we label the type of those variables as any.

let notSure: any = 4;
notSure = "Why not a string now!"; // ok
notSure = false; // ok

text

The any type can allow you to work with existing javascript. Allowing you to gradually opt in and opt out of type checking during compilation.

Difference between Any and Object

You might expect Object to be as obscure as any, but the difference is that whereas TypeScript will not complain on arbirary property/method calls to an any variable, it will definitely not allow you to call arbitrary methods on variables of type Object. That's mainly because variables of type Object can host any type of object as long as typeof v === 'object', but once one is placed into the variable, only methods of that specific object will be allowed to be called by TypeScript.

Check difference between Object and object in TypeScript.

let notSure: any = 4;
notSure.ifItExists(); // Ok - ifItExists might be available at runtime

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

text

You can also have type any arrays, which allow the array to contain anything

let a: any[] = [1, "b", new Hey()];
a.push("Something Else");

text

Void

void is somewhat like the opposite of any the absence of having any type at all. You may commonly see this as the return type of functions that do not return a value. Or which return undefined in JavaScript. So basically void is like the type undefined but for function returns. Actually you can also use it for variables, and it will let you store null (only if --strictNullChecks is not specified) or undefined.

function f(): void {
  console.log("No return value");
}
let unusable: void = undefined; // what's the point? None
unusable = null; // only if --stricNullChecks is not enabled, but still useless

text

Null & Undefined

null and undefined are actually their own types. Much like void they are not extremely usefull on their own.

let u: undefined = undefined; // only acceptable value for u
let n: null = null; // only acceptable value for n

text

By default null and undefined are included in all other types. That means you can assign null and undefined to something that is of type number for example.

Note: if --strictNullChecks is enabled, then null and undefined are only assignable to variables of type any (well undefined can also be assigned to void). In that case you need to use type unions in order to allow having variables that can hold string and null and undefined with: string | null | undefined.

Object

object (with lowercase o) is a type that represents the non primitive types i.e. anything that is not in:

  • number
  • string
  • boolean
  • symbol
  • null
  • undefined

With object type, API's like Object.create can be better represented. For example:

declare function create(o: object | null): void;
create({prop: 0}); // Ok
create(null); // Ok because we added the union

create("hello"); // Error
create(54); // Error
create(undefined); // Error
create(false); // Error

text

Object vs object

object with lowercase o is anything that is non primitive type. Whereas Object are the functionalities of every object in javascript, like .hasOwnProperty(s: string): boolean or .toString(): string etc. Here is the definition of type Object:

interface Object {
  // ...

  /** Returns a string representation of an object. */
  toString(): string;

  /** Returns a date converted to a string using the current locale. */
  toLocaleString(): string;

  /** Returns the primitive value of the specified object. */
  valueOf(): Object;

  /**
   * Determines whether an object has a property with the specified name.
   * @param v A property name.
   */
  hasOwnProperty(v: string): boolean;

  /**
   * Determines whether an object exists in another object's prototype chain.
   * @param v Another object whose prototype chain is to be checked.
   */
  isPrototypeOf(v: Object): boolean;

  /**
   * Determines whether a specified property is enumerable.
   * @param v A property name.
   */
  propertyIsEnumerable(v: string): boolean;
}

text

Type assertions

Sometimes you will end up in a situation where you will know better about a value than TypeScript does. This usually will happen when you know the type of some entity could be narrower than its current type.

Type assertions are a way to tell the complier "trust me, I know what I'm doing!". A type assertion is like a type cast in other languages, but performs no special checking or restructuring of the data. It has no runtime impact and is used purely by the compiler. TypeScript assumes that you, the programmer, have performed any special checks that you need.

Type assertions have two forms:

  • <> angle bracket (not allowed with JSX)
  • as syntax (works with JSX)
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
strLength = (someValue as string).length;

text

Both notations are equivalent. But when using TypeScript with JSX, only the as notation is allowed.

Tuple destructuring

let tuple: [number, string, number] = [1, "one", 2];
let [a, b, c] = tuple; // a: number, b: string, c: number

text

cannot attempt to destructure more than what the tuple contains

let tuple: [number, string, number] = [1, "one", 2];
let [a, b, c, d] = tuple; // Error: no element at index 3, whereas in Javascript this would just make e === undefined

text

except if you destructure it into a rest, which produces an empty tuple

let tuple: [number, string, number] = [1, "one", 2];
let [a, b, c, ...d] = tuple; // a: number, b: string, c: number, d: [] empty tuple

text

You can also destructure a tuple into a smaller tuple with the rest operator

let tuple: [number, string, number] = [1, "one", 2];
let [a, ...b] = tuple; // a: number, b: [string, number]
let [ , , c] = tuple; // c: number

text

Object destructuring

As with array destructuring. An interesting feature is destructuring without declaration (note the user of parenthesis, Javascript parses { as the start of a block):

let a: string, b: number;
({a, b} = {a: "hi", b: 12});

text

Object property renaming

You can destructure object properties into variables with different names than the properties themselves. But note how using the colon does not mean the type but rather the name of the variable

let o = {a: "hi", b: 12};
let {a: renamedToThis, b: renamedToThat} = o;
console.log(renameToThis); // "hi"

text

You can still specify the type with

let o = {a: "hi", b: 12};
let {a, b}: { a: string, b: number } = o;

text

Default values

Default values let you specify the value in case a property is undefined:

function hello(wholeObject: { a: string, b?: number }): voic {
  let { a, b = 100 } = wholeObject;
}

text

Function Declarations

Destructuring also works in function declarations.

type C = { a: string, b?: number };
function hello({ a, b }: C): void {
  let { a, b = 100 } = wholeObject;
}

text

But specifying defaults is more common for parameters, and getting defaults right can be tricky. First of all you need to remember to put the pattern before the default value. (This is an example of type inference)

function f({ a = "", b = 0 }: {}): void {
  //...
}

text

Then you need to remember to give a default for optional properties on the destructured property instead of the main initializer. Remember that C was defined with b optional.

function f({ a, b = 0 } = { a = "" }): void {
  console.log(a, b);
}

f({}); // Error - 'a' needs to be present in the supplied argument object
f({b: 1}); // Error - 'a' needs to be present in the supplied argument object
f(); // Good - the default parameter { a = "" } will be used and default destructured 'b' value as well, logs: "", 0
f({a: "asdf"}); // Good - the default parameter { a = "" } will be overridden and default destructured 'b' value will be used, logs: "asdf", 0
f({a: "asdf", b: 1 }); // Good - the default parameter { a = "" } will be overridden and default destructured 'b' as well, logs: "asdf", 1

text

The above is using destructuring (on the left of the middle =) for the function's single parameter, which destructured into { a, b = 0 } where b is optional. Then the right side of the middle = is giving a default value to that single parameter with { a = "" }. Since b has a default value inside the destructuring, the resulting single parameter object will have a b property = 0 if the function f is not called with an object containing a property b (in which case, that b will override the default).

Spread

Arrays

let first = [1, 2]
let second = [4, 5]

let combined = [0, ...first, 3, ...second, 6]

text

Objects

let defaults = { food: "spicy", price: "$", ambiance: "noisy", };
let search = { ...defaults, food: "rich", value: "Hey", }; // food overrides defaults: good
// { food: "rich", price: "$", ambiance: "noisy", value: "Hey"}
let search2 = { food: "rich", ...defaults, }; // defaults override food, which is not what we want
// { food: "spicy", price: "$", ambiance: "noisy", value: "Hey"}

text

Object spreads have limitations:

  1. It only spreads own enumerable properties: you loose methods
  2. TypeScript compiler does not allow spread of type parameters from generic functions. That feature is expected in future versions of the language

Interfaces bis

TypeScript is a "Duck Typing" meaning that if it "flies and quacks like a dock, then it is a duck". It's not the name of the interface that matters, it's the actual shape of the object that is being passed in the call that matters. It has to at least conform, but can also have more properties than the required ones.

function printLabel(labeledObject: { label: string }) {
  console.log(labeledObject.label);
}

printLabel({ a: "asdf", label: "Some Label" }); // Ok : "Some Label"
printLabel({ a: "asdf" }); // Error

text

Now the same example using loger notation, with explicit interface declaration

interface Labeled {
  label: string;
}
function printLabel(labeledObject: Labeled) {
  console.log(labeledObject.label);
}
printLabel({ a: "asdf", label: "Some Label" }); // Ok : "Some Label"
printLabel({ a: "asdf" }); // Error

text

Optional properties

interface SquareConfig {
  color?: string;
  width?: number;
}
function createSquare(config: SquareConfig): { color: string, area: number } {
  let square = { color: "white", area: 10 };
  if (config.color) {
    square.color = config.color;
  }
  if (config.width) {
    square.area = config.width * config.width;
  }
  return square;
}
let s = createSquare({ width: 100 }); // { color: "white", area: 10000 }

text

Readonly properties

Some properties should only be modifiable on creation, and then only readable. You can use interfaces for that with the keyword readonly.

interface Point {
  readonly x: number;
  readonly y: number;
}
let p: Point = {x: 1, y: 1};
p.x = 5; // Error readonly !

text

By assigning an object literal to a variable with type Point, we make it's properties unchangeable (readonly).

Readonly Array

Typescript comes with the ReadonlyArray<T> which is the same as Array<T> with all mutating methods removed, making sure you do not change the Array after creation.

let a: number[] = [1, 2, 3]
let ro: ReadonlyArray<number> = a;
ro.push(4); // Error !
ro[0] = 4; // Error !
ro.length = 100; // Error !
a = ro; // Error ! cannot assign ReadonlyArray<number> to Array<number>
a = ro as number[]; // Ok type assertion (type casting) is allowed

text

Readonly vs. const

readonly is to properties what const is to variables.

Excess Property Checks

Optional properties in interfaces give you the possibility to not pass them. And what can happen is that if you try to pass a misspelled property as an object literal, you might inadvertently insert a bug.

interface SquareConfig {
  color?: string;
  width?: number;
}
function createSqaure(config: SquareConfig): { color: string, area: number } {
  // ...
}
let s = createSquare({ colour: "Orange" }); // error: Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?

text

TypeScript does excessive property checking when passing an object literal as the parameter. This is to avoid misspelled properties in the object literal. You can easily get around these checks with type assertions

let s = createSquare({ colour: "Orange" } as SquareConfig); // Works, but with a bug inserted !

text

Another approach is to allow additional properties within the interface itself, without mentioning their explicit names. The following allows having both color and width with their respective types, but additionally any other property of any type, this is using index signatures.

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}

text

Another option is to assign the object literal to some variable. Since the variable is no longer an object literal, typescript will no longer perform excessive type checks so the compiler will not give you any error.

let config = { colour: "Orange" };
let s = createSquare(config); // works fine but with a bug inserted !

text

In the majority of cases, excess property errors are actually caused by bugs, so you should not try to bypass those checks. And rather revise your type definitions and add properties to your types if needed. However, for more complex object literals that have methods and hold state, you might need to keep these techniques in mind.

Function Types

interface SearchFunc {
  (source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function(src: string, subStr: string): boolean {
  let result = src.search(subStr);
  return result > -1;
}

text

Note: type checks pass even if the parameter names do not match. Only types in parameter positions need to match.

Function parameters are checked one at a time, with the type in each corresponding position checked against each other. If you do not want to specify types at all, TypeScript's contextual type checking can infer types, since the function value is assigned directly to a variable of type SearchFunc. Here also the return type of our function expression is implied by the values it returns (here false and true). Had the function expression returned or numbers or strings, the type checker would have warned us that the type did not match the return type described in SearchFunc.

let mySearch: SearchFunc;
mySearch = function(src, substr) {
  let result = src.search(substr);
  return result > -1;
}

text

Indexable Types

Similarly to how we can use interfaces to describe function types, we can also describe types that we can "index into" like a[10], or ageMap["Daniel"]. Indexable types have an index signature that describes the types that we can use to index into an object, along with the corresponding return types when indexing. Let's take an example;

interface StringArray {
  [index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];

text

Above we have a StringArray interface that has an index signature. This index signature states that when StringArray is indexed with a number it will return a string.

There are two types of supported index signatures: string and number. It is possible to support both types of indexers, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing with a number, Javascript will actually convert that to a string before indexing into an object. That means that indexing with 100 (a number) is the same as indexing with "100" (a string), so the two need to be consistent.

class Animal {
  name: string;
}

class Dog extends Animal {
  breed: string;
}

interface NotOkay {
  [x: number]: Animal;
  [x: string]: Dog; // Error: getting an animal with numeric index might return you a totally different type of Animal (numeric index should return Animal, but since it is converted to string first, and strings return Dogs, the numeric may return Dog as well.)
}

text

While string index signatures are a powerful way to describe the "dictionary" pattern, they also enforce that all properties match their return types. This is because a string index declares that obj.property is also available as obj["property"]. In the following example, name's does not match the string's index type, and the type checker gives an error.

interface NumberDictionary {
  [index: string]: number;
  length: number; // Ok length is a number, matches above
  name: string; // Error: name is not a number, does not match the string index type `number`
}

interface NumberDictionary {
  [index: string]: number | string;
  length: number; // Ok length is a number, matches above
  name: string; // Ok`both are tolerated now
}

text

We can also make index signatures readonly in order to prevent assignments to their indices.

interface ReadonlyStringArray {
  readonly [index: number]: string;
}
let sa: string[] = ["Hey", "These", "Elements", "Will", "Never", "Change"]
sa[0] = "Change Now!"; // Error - index signature is readonly

text

Class Types

Implementing in interfaces

One of the most common uses of interfaces in languages like C# and Java, that of explicitly enforcing that a class meets a particular contract is also possible in Typescript.

interface ClockInterface {
  currentTime: Date;
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  constructor(h: number, m: number) {}
}

text

You can also describe methods in an interface that are implemented in the class, as we do setTime in the below example:

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  setTime(d: Date): void {
    this.currentTime = d;

  }
  constructor(h: number, m: number) {}
}

text

interfaces describe the public side of the class, rather than both the public and private side of the class. This prohibits you from using the to make sure that a class uses specific types for the private side of the class.

Difference between Static and Instance side of classes

When working with classes and interfaces it is helpful to keep in mind that the class has two types: the type of the static side and the type of the instance side. You may notice that if you try to create an interface with a construct signature and try to create a class that implements this signature, you get an error.

interface ClockConstructor {
  new (hour: number, minute: number);
}

// Error!
class Clock implements ClockConstructor {
  currentTime: Date;
  constructor(h: number, m: number) {}
}

text

This is because when a class implements an interface only the instance part of the class is checked against the interface. Since the constructor sits in the static side of the class it is not included in the check. Therefore the interface instance requirement is not met by the class in the example above.

Instead, you would need to work with the static side of the class directly. In the following example, we define two interfaces: ClockConstructor for the constructor and ClockInterface for the instance side of the class.

interface ClockConstructor {
  new (hour: number, minute: number);
}

interface ClockInterface {
  tick(): void;
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
  return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
  currentTime: Date;
  constructor(h: number, m: number) {}
  tick() {
    console.log('beep beep');
  }
}

class AnalogClock implements ClockInterface {
  currentTime: Date;
  constructor(h: number, m: number) {}
  tick() {
    console.log('tick tock');
  }
}

let d = createClock(DigitalClock, 12, 0);
let a = createClock(AnalogClock, 7, 32);

text

Because createClock first parameter is of type ClockConstructor the argument passed to it, must implement it. We know from ES5 to ES6 that class notation is syntactic sugar for simple the former function name notation, this is useful to understand that the name of the class, is actually a reference to the function containing the constructor in it's prototype (which is invoked with the new keyword).

Another simple way is to use class expressions:

interface ClockConstructor {
  new (hour: number, minute: number);
}

interface ClockInterface {
  tick(): void;
}

const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log('beep beep');
  }
}

text

Here we do the same thing, we first create the class Clock on the right hand side of the = enforcing the implementation of the ClockInterface, then we store the whole expression into a constant Clock on which we enforce the ClockConstructor. By the same reason as above, what we store in const Clock is actually the constructor function, which has a prototype with the tick and constructor etc.

Extending interfaces

Like classes, interfaces can extend each other:

interface Shape {
  color: string;
}
interface Square extends Shape {
  sideLength: number;
}

text

You can compose interfaces from multiple others:

interface Shape {
  color: string;
}
interface Bounceable {
  bounce(x: number, y: number, force: number): void;
}
interface BouncableSquare extends Shape, Bounceable {
  sideLength: number;
}

text

Hybrid types

As mentioned earlier, interfaces can describe rich types present in real world javascript. Because of JavaScript's dynamic and flexible nature, you may occasionally encounter an object that works as a combination of some of the types described above.

One such example is an object that acts both as a function and an object, with additional properties.

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

function getCounter(): Counter {
  let counter = (function(start: number) {}) as Counter;
  counter.interval = 100;
  counter.reset = function() {};
  return counter;
}

let counter = getCounter();
counter(10);
counter.reset();
counter.inverval = 10;

text

When interacting with 3rd party Javascript libraries you may need to use patterns like these to fully describe the shape of the type.

Interfaces Extending Classes

When an interface type extends a class type, it inherits its members but not their implementation. Interfaces even inherit the private and protected members of the base class. This means that when you create an interface that extends a class with private or protected members, that interface can only be implemented by that class or subclass of it.

This is useful when you have a large inheritance hierarchy, but want to specify that your code works with only subclasses that have certain properties. The subclasses don't need to be related, other than inheriting from the base class.

class Control {
  private state: any;
}

// SelectableControl inherits the private state from Control
interface SelectableControl extends Control {
  select(): void;
}

// Ok because SelectableControl is checking if Button has a private state
// originating in Control, which it does because Button extends Control
// but Button has no direct access to Control's private state, yet
// what SelectableControl checks is whether Button has private state
// in its prototypical chain, and if it originates in Control, which
// it does.
class Button extends Control implements SelectableControl {
  select() {}
}

// TextBox inherits the private state only Control has access to it though
class TextBox extends Control {
  select() {}
}

// Error - property state is missing in type 'Image'
// The problem here is that SelectableControl does not find
// a private state originating in Control, therefore it crashes
// even though Image has a private member state, just not originating
// in the same declaration as SelectableControl takes it from (Control).
class Image implements SelectableControl {
  private state: any;
  select() {}
}

class Location {

}

text

Only descendants of Control will have a state private member that originates in the same declaration, which is a requirement for private members to be compatible. Within the Control class it is possible to access the state private member through an instance of SelectableControl.

Intersection types

So if you have an intersection between two types, basically you have a narrower type, but with broader properties:

type Duck = Quackable & Swimable; // Duck has the union of Quackable and Swimable properties

text

A duck is only a duck if it can both quack and swim. If it' can only do one of the two, it is not a duck. What you get is the union of all properties, which is counterintuitive, but since there are more properties, less objects conform to it which makes it narrower…

Union types

On the other hand, if you have a union between two types, basically you have a broader type, but narrower set of property requirements:

type Flyable = Eagle | Butterfly; // Flyable has the intersection of Eagle and Butterfly

text

An Eagle can fly and pray, and the butterfly can fly and float. The intersection of the properties is the fly property. The union of these is therefore a smaller property set, thus a broader set of matching objects.

Custom type guards

Let's say you do not want to cast a type, but there is some custom logic that you require to know whether some object belongs to a type. So you can do something like this:

const canFly = (animal: Animal): animal is Flyable => typeof (animal as any).fly === 'function'
if (canFly(myAnimal)) {
  animal.fly();
}

text

with the above, you can tell typescript that it can assume that this is actually a flyable. It is kind of a safer cast, but it depends on how safe your implementation of this function is. Another example.

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

text

pet is Fish is our type predicate in this example. A predicate takes the form of parameterName is Type where parameterName must be a parameter name from the current function's signature. Any time isFish is called with some variable as in:

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

text

typescript will narrow that variable down to that specific type, if the variable's original type is compatible. Note that typescript not only knows that pet is a Fish in the if branch; it also knows that in the else branch, if you don't have a Fish then it must be a Bird (Because it would have thrown a compilation error if the parameter passed to isFish() did not belong to the union type: Fish | Bird).

Using the in operator

The in operator acts as a narrowing expression for types. In the n in x expression, n is a string literal and x is a union type.

function move(pet: Fish | Bird) {
  if ("swim" in pet) {
    return pet.swim();
  }
  return pet.fly();
}

text

typeof typeguards

function isNumber(x: any): x is number {
  return typeof x === "number";
}
function isString(x: any): x is string {
  return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
  if (isNumber(padding)) {
    return Array(padding + 1).join(" ") + value;
  }
  if (isString(padding)) {
    return padding + value;
  }
  throw new Error(`Expected string of number got ${padding}`);
}

text

You actually don't need to write a function to let Typescript know that you are typeguarding. You can write them inline. The only requirement is that the typenames you compare to are in: "number", "string", "boolean", or "symbol".

instanceof type guards

instanceof is a way to narrow down a type by the constructor function. For example:

interface Padder {
  getPaddingString(): string;
}
class SpaceCountPadder implements Padder {
  constructor(private numSpaces: number) { }
  getPaddingString(): string {
    return Array(this.numSpaces + 1).join(" ");
  }
}
class StringPadder implements Padder {
  constructor(private value: string) { }
  getPaddingString(): string {
    return this.value;
  }
}
function getRandomPadder() {
  return Math.random() > 0.5 ? new SpaceCountPadder(5) : new StringPadder("---");
}
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceCountPadder) {
  padder; //constructed with SpaceCountPadder
}
if (padder instanceof StringPadder) {
  padder; //constructed with StringPadder
}

text

The right side of instanceof must be a constructor function and typescript will narrow down to:

  1. the type of the function's prototype property if its type is not any
  2. the union of types returned by that type's construct signature in that order.

Nullable types

Typescript has two special types null and undefined that have the values null and undefined respectively. By default, the type checker considers these two types assignable to anything. Effectively, null and undefined are valid values of every type. That means that it si not possible to stop them from being assigned to variables of any type. The inventor of null Tony Hoare calls this his billion dollar mistake. That is why typescript has the --strictNullChecks, when you declare a variable, it's type does not include null or undefined, which means you cannot assign them unless you explicitly include them via a union.

let s = "hello";
s = null; // error : 'null' is not assignable to 'string'
let sn: string | null = "world";
sn = null; // works
sn = undefined; // error : 'undefined' is not assignable to 'string | null'

text

Note: TypeScript treats null and undefined as different in order to match JavaScript semantics. So string | null is different than string | undefined and string | null | undefined.

With --strictNullChecks an optional parameter automatically adds | undefined:

function f(x: number, y?: number): number {
  if (y === undefined) {
    return x;
  }
  return x + y;
}
f(1, 2); // works
f(1); // works
f(1, undefined); // works
f(1, null); // error: 'null' is not assignable to 'number | undefined'

text

The same is true for optional properties.

class C {
  a: number;
  b?: number;
}
let c = new C;
c.a = null; // error : 'null' is not assignable to 'number'
c.a = unefined; // error : 'undefined' is not assignable to 'number'
c.b = null; // error : 'null' is not assignable to 'number'
c.b = undefined; // works

text

Type guards and type assertions

Since nullable types are implemented with a union, you need to use typeguard to get rid of the null.

function f(sn: string | null): string {
  if (sn == null) {
    return "default"
  }
  return sn;
}

text

The null elimination is pretty obvious here, but you can use terser operations:

function f(sn: string | null): string {
  return sn || "default";
}

text

! for known non-null

In cases where the compiler can't eliminate null or undefined, you can use type assertion operator to manually remove them. The syntax is postfix !: identifier! removes null and undefined from the type of identifier. In the example below, children[0] could potentially be undefined if children is empty for example. So what we do with the ! is tell typescript that children[0]'s type does not contain undefined or null. And so it will contain the rest of types with which it was defined.

person.children[0]!.name; // here the developer tells ts that there will always be an element within the children array.

text

Another example using nested functions. We use nested functions because the compiler does not know what the value of the outer function's param will be before it is being called. So the compiler cannot eliminate nulls inside a nested function except if it is an IIFE which it isn't here :

function broken(sn: string | null): string {
  function postFix(epithet: string) {
    return sn.charAt(0) + '. the' + epithet; // error: 'sn' is possibly 'null'
  }
  sn = sn || "Bob";
  return postFix("Great");
}
function fixed(sn: string | null): string {
  function postFix(epithet: string) {
    return sn!.charAt(0) + '. the' + epithet; // works
  }
  sn = sn || "Bob";
  return postFix("Great");
}

text

Literal Types

You can actually use values as types. So if types are sets, a type defined with 3 values, is just the set of 3 values that conform to the type.

type Move = 'ROCK' | 'PAPER' | 'SCISSORS';

text

Type string is the set of all strings, and type Move above is just the set of these 3 strings. So:

let a: Move = 'HEY'; // Error: cannot assign 'HEY' to type Move
let b: Move = 'PAPER'; // Works!

text

Never

The never type represents the type of values that never occur. For instance, never is the return type for a function expression that will always throw an exception or one that never returns (like an infinite loop).

function myError(message: string): never {
  throw new Error(message);
}
function fail() {
  return myError('Something failed'); // inferred return type is never
}
function loop(): never {
  while (true) {
    console.log("this will never stop");
  }
}

text

never is the return type of a function that does not return. For example if you throw an error or you loop, that is a never

const throws = (): never => {
  throw new Error('this never returns');
}
const loops = (): never => {
  while (true) {
    console.log('a piece of eternity, this function never returns');
  }
}

text

Unknown

unknown is a special type that forces you to do castings before being able to be assigned to a variable of another type. Use unknown for example when a user provides a value which you do not know the type in advance.

let a: string;
let x: any;
a = x; // any assigned to a string compiles
x = a; // string assigned to any compiles

let y: unknown;
y = a; // unknown accepts string, compiles
a = y; // Error: string does not accept unknown, NOT COMPILES

text

Index Types

keyof MyType gives the set of all key types. From there you can retrieve the type of a specific key. And with all the keys you can get the whole union of types.

type Duck = {
  color: string;
  feathers: number;
}

type DuckProps = keyof Duck; // = 'colors' | 'feathers'
type ColorType = Duck['color']; // = string
type DuckValues = Duck[DuckProps]; // = string | number

text

Conditional Types

Depending on T, it will resolve to a type or another one. In the example below, if we pass a a boolean type to the variable type in StringOrNumber then we get a string type.

type StringOrNumber<T> = T extends boolean ? string : number;
type T1 = StringOrNumber<true>; // string
type T2 = StringOrNumber<false>; // string
type T3 = StringOrNumber<object>; // number

text

This is a small example to get the type name of a specific type:

type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type T0 = TypeName<string>; // "string"

text

The TypeName of a string is the string "string"

Infer

infer is a special keyword that you can use within a conditional type.

type ElementType<T> = T extends (infer U)[] ? U : never;
type T0 = ElementType<[]>; // = never
type T1 = ElementType<string[]>; // = string

text

Here we are using infer which is used to the right side of extends. What it means is : if T is an array of something, infer the type of that something (U) and resolve to that something (U); otherwise resolve to never. T0 resolves to never because T is an array of nothing since it has no arguments. T1 on the other hand.

Misc Patterns

Functional Programming

There is a difference between data and an object. The data is just pure data, whereas the object usually has methods. But the reality is we don't actually need objects, we only need functions that perform operations on those data structures. The cool thing is that you don't need to define any classes or constructors for data structures, you just need to define what kinds of values are there.

In the example below, typescript will tell you if you do any mistake (meaning that you don't need any constructors or any class etc), only the function that transforms one structure into the next.

interface RawPerson {
  identifier: number;
  first_name: string;
  last_name: string;
}

interface Person {
  id: string
  fullName: string;
}

const transformPerson = (raw: RawPerson): Person => {
  return {
    id: `${raw.identifier}`,
    fullName: `${raw.first_name} ${raw.last_name}`,
  };
}

text

Discriminate Unions

So we have talked about unions, this is how unions look like. What the union does, is for common properties, it will unite the property values.

type Eagle = {
  kind: 'eagle';
  fly: () => 'fly';
};

type Duck = {
  kind: 'duck';
  quack: () => 'quack';
};

type Bird = {
  kind: 'bird';
};

type Animal = Eagle | Duck | Bird; // If the object has any of the above props

let a: Animal = {kind: 'a', }; // Error : Type '"a"' is not assignable to type '"eagle" | "duck" | "bird"'
let b: Animal = {kind: 'bird', }; // b is of type Bird
b.quack(); // Error: property quack() does not exist on type 'Bird'

text

How can you discriminate unions?

const doSomething = (animal: Animal): string => {
  switch (animal.kind) {
    case "eagle":
      return animal.fly();
      break;
    case "duck":
      return animal.quack();
      break;
    default:
      return assertNever(animal);
  }
};

const assertNever = (animal: Animal): never => {
  throw new Error(`unknown Animal, ${animal.kind}`);
}

text

In the above example, if we had not added a default then the doSomething return type should have been string | void because the "bird" case would not be handled, and so anytime a Bird was passed, doSomething would return undefined. To solve this, we can add the default case which makes sure that when an Animal not in the switch case list is passed (such as Bird) an error will be thrown. This is achieved returning a call to our assertNever which has return type never meaning it throws every time. Since never is no return, we can keep the string return type in the doSomething.

Derive Types from Constants

const MOVES = {
  ROCK: { beats: 'SCISSORS', },
  PAPER: { beats: 'ROCK', },
  SCISOR: { beats: 'PAPER', },
};

// Lets say we want to define the type Move which is the set of strings: "ROCK" | "PAPER" | "SCISSORS"

type MoveBad = keyof MOVES; // any 'MOVES' refers to a value, but is being used as a type here.

// the above does not work that is why we need to add a `typeof` keyword which gets the type of the moves constant
// and then retrieve the keys of the type (typeof MOVES), we do that with the help of `keyof`

type Move = keyof typeof MOVES;
const move: Move = 'ROCK';

text

Untrusted user input

If something is unknown you want perform some kind of validation, and make the unknown type comply to some known type and return that. In the following example we are using the typeof keyword which works like in javascript.

const validateInput = (s: unknown): number => {
  let n;
  switch (typeof s) {
    case 'number':
      // handle and return a number
      break;
    case 'string':
      // handle and return a number
      break;
    default:
      throw new Error('This kind of input is not supported');
  }
}

text

Mapped types

Readonly

type Readonly<T> = { readonly [P in keyof T]: T[P] };
type ReadonlyDuck = Readonly<Duck>; // { readonly color: string; readonly feathers: number; }

text

Partial

type Partial<T> = { [P in keyof T]?: T[P] };
type PartialDuck = Partial<Duck>; // { color?: string; feathers?: number; }

text

Required

type PartialDuck = {
  color?: string;
  feathers?: number;
}

type Required<T> = { [P in keyof T]-?: T[P] };
type RequiredDuck = Required<PartialDuck>; // { color: string; feathers: number; }

text

Pick

type Pick<T, K extends keyof T> = { [P in K]: T[P] }
type ColorDuck = Pick<Duck, 'color'>; // { color: string; }

text

Pick SQL

async function fetchPersonById<T extends keyof Person> (
  id: string,
  ...fields: T[]
): Promise<Pick<Reaction, T>> {
  return await connection('Person')
    .where({ id })
    .select(fields)
    .first();
}

const id = 'asdfasdf';
const reaction = await fetchPersonById(id, 'name', 'age'); // = { name: string, age: number }

text

Generics

Generics are here to allow you to have components that can work on a variety of types rather than a single one. This allows users to consume the components with their own types.

Hello World of Generic

The identity function is a good example for the generic type. The identity function is like in matrix algebra, a function that returns the input itself. We could achieve this in two ways without generics:

  1. Adding a type to the input and the same to the return. Which restricts the possible inputs.
   function identity(input: number): number {
     return input;
   }
  1. Use the any type to allow any type. This is generic, but it will make us lose information about the input type: when the function returns, whatever type the input was, typescript will now have unconstrained it to any.
   function identity(input: any): any {
     return input;
   }

Instead, with generics we can allow both:

  • Allowing any input type
  • Not loosing the input's type on return
function identity<T>(input: T): T {
  return input;
}

text

Notice the <T> is a type variable, when calling the identity function, the user can specify the type he wants that variable to hold explicitly:

const s = identity<string>("Hello");

text

Above you can see that the user specified that the type variable T should hold the type string. But we can also let the compiler figure out the type implicitly the type of the argument:

const s = identity("Hello");

text

The above uses type argument inference, where the compiler sets the value of T, here it will be set to string by the compiler.

Argument's shape

Typescript needs to know what are the available methods of the generic argument before you can use them. Otherwise it will complain that T does not have the specified method like below:

function consoleLogIdentity<T>(input: T): T {
  console.log(input.length); // Error: T does not have .length
  return input;
}

text

Because before consoleLogIdentity is called, there is no way for TypeScript to know if input of unknown type T has any method at tall.

Let's say we have intended the consoleLogIdentity function to be called on arrays of what the T variable type will hold when it is called. We can then define it as:

function consoleLogIdentity<T>(input: T[]): T[] {
  console.log(input.length);
  return input;
}

text

input is now of type Array of some type. So the .length prop is available and the code logs the number of elements of the array. An alternative writing for this is:

function consoleLogIdentity<T>(input: Array<T>): Array<T> {
  console.log(input.length);
  return input;
}

text

This is the same as the same as the previous code just with a different notation.

Generic Interfaces

Previously we created generic identity functions that worked over a range of types. Now we will explore the type of the functions themselves and how to create generic interfaces. The type of generic function is just like those of non-generic functions, with the type parameters listed first, similarly to function declarations:

function identity<T>(input: T): T {
  return input;
}
let myIdentity: <U>(inp: U) => U = identity;

text

Above, note that the name of the variable need not match. T or U does not matter so long as the number of type variables and how the are lined up match. We can also write the generic type as a call signature of an object literal type:

function identity<T>(arg: T): T {
  return arg;
}
let myIdentity = {<T>(arg: T): T} = identity;

text

Which leads us to writing our first generic interface. Let's take the object literal from above and move it to an interface:

interface GenericIdentityFn {
  <T>(input: T): T
}
function identity<T>(arg: T): T {
  return arg;
}
let myIdentity: GenericIdentityFn = identity;

text

We may also want to move the generic parameter to be a parameter of the whole interface like so:

interface GenericIdentityCustomType<T> {
  (arg: T): T;
}
function identity<T>(input: T): T {
  return input;
}
let myNumIdentity: GenericIdentityCustomType<number> = identity;

text

myNumIdentity now only accepts number as first argument, we have "casted" identity to accept only type number as first argument. So myNumIdentity is not a generic at all.

Generic Classes

Generic classes have a similar shape as generic interfaces. Generic classes have a generic type parameter list in angle brackets following the name of the class:

class GenericSet<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
  // add: (x: T, y: T) {}
}

let integer = new GenericSet<number>();
integer.zeroValue = 0;
integer.add = function(x, y) { return x + y; };

text

This is a pretty literal use of the GenericSet class. But nothing stops us from creating a set for strings:

let str = new GenericSet<string>();
str.zeroValue = "";
str.add = function(s1, s2) { return s1 + s2; };

console.log(str.add(str.zeroValue, "Something else"));

text

Note: Generic classes can work only on the instance part of the class.

Type Inference

Type is what Typescript does when no explicit type annotation. In its simplest form:

let x = 2;

text

Here typescript infers that x has type number.

Best common type

The best common type arises in cases where the type needs to be inferred from a set of expressions like in:

let a = [1, 2, null];

text

Here typescript will look at the set of types within the array: number, number and null. So there is a common type between the two first elements (number), but third type is null which is not within the number type. In cases where typescript finds variables with different types, it will create a union, here: number | null.

Typescript will not go as far as to find a common ancestor:

let zoo = [new Rhino, new Giraffe, new Lion]; // [ts] zoo: Rhino | Giraffe | Lion

text

Even though all objects extend from Animal, typescript will use the union of each class instead. To force using the Animal type you need to be explicit:

let zoo: Animal[] = [new Rhino, new Giraffe, new Lion]; // [ts] zoo: Animal[]

text

Contextual typing

Is type inference but instead of going from right to left, it goes from left to right:

window.onmousedown = function(ev) {
  console.log(ev.button); // ok
  console.log(ev.someMethodNotBelongingToMouseEvent); // [ts] Error (not when I tried, contextual typing didn't work for me)
}

text

A better example is again with the zoo where the return type has a few candidates from the return expression plus the return type annotation:

function createZoo(): Animal[] {
  return [new Rhino(), new Crocodile(), new Loris()];
}

text

Here the contextual typing inferences has 4 candidates for the return type: Animal, Rhino, Crocodile and Loris, which all have a common type ancestor: Animal. Therefore Animal is finally chosen by typescript.

Type Compatibility

Type compatibility in TypeScript is based on structural subtyping: a way of relating types based on their members. The rules is:

x is compatible with y if y has at least the same members as x.

IMPORTANT: structural typing is different from duck typing. Structural typing is a static typing system that determines type compatibility and equivalence by the type's structure. Whereas duck typing is dynamic and determines type compatibility by only that part of a type's structure that is accessed during run time.

Sound, Unsound, Soundness, Unsoundness

Soundness means that if the type system says that a variable has a particular type, then it definitely has that type at runtime. A sound system is always correct, and an unsound type system might be incorrect in some cases.

function pushANumber(a: (string | number)[]): void {
  a.push(100);
}
const strings: string[] = ["one", "two"];
pushANumber(strings);

const myFakeString: string = strings[strings.length]
myFakeString.toLowerCase(); // [ts] ok but ;; [js] Uncaught TypeError: myFakeString.toLowerCase is not a function

text

In the above code, typescript is unable to detect that myFakeString is actually a number (here 100). So typescript is unsound.

TypeScript's type system allows certain operations that can't be known at compile time to be safe. When a type system has this property, it is said to not be "sound".

The places where typescript allows unsound behavior where carefully considered and trhought this document we'll explain where these happen and the motivating scenarios behind them.

interface Named {
  name: string;
}

let x: Named;
const john = {name: "john", sex: "male"};
x = john; // works
function callName(n: Named): void {
  console.log(n.name);
}
callName(john); // works

text

Typescript will check each property of x and make sure there is a corresponding compatible property in john. Which there is so ti works. It works exactly the same with function parameters, where in callName: n takes the place of x, so typescript checks whether each property of n is present in the supplied param john.

Comparing two functions

Comparing primitive and object types is pretty straightforward.

Checking the compatibility of functions is more involved.

let one = (a: number) => a + "";
let two = (b: number, s: string) => a + s;

one = two; // [ts] Type '(a: number, s: string) => number' is not assignable to type '(a: number) => number'
two = one; // [ts] ok

two(1, "2"); // [ts] ok but parameter 2 is ignored in one
two(1); // [ts] Error! Expected 2 arguments, but got 1

text

You can see that one is assignable to two but the reciprocal is not true. Why? Because in javascript, ignoring extra function parameters is quite common, like Array#forEach:

Even though two is assigned a one, the type of two remains the same, meaning that you must still call it with 2 parameters even though the function one does not use the second parameter. So the reduced parameter call is not allowed within typescript. See the Array#forEach example:

let items = [1, 2, 3];

// passing a fully defined callback
items.forEach((item, index, array) => console.log(item));

// passing a callback with only those parameters in use
items.forEach((item) => console.log(item));

text

So to think back of our previous example above, with one and two, in the second items.forEach() call we are passing a one (the callback) into a two (the foreach parameter).

The key point here, is that we are assigning a function to a variable like before one to two (but here the two is the forEach parameter). Remeber that we were not allowed to call two(1) with a single parameter, so how is forEach going to do it? It does so out of typescript's reach, inside the V8.

Comparing function return types

Here we are going to compare two functions where the returned objects differ in one having a member that the other does not have, but the both share a common member:

let retOne = () => ({ common: "he"});
let retTwo = () => ({ common: "di", onlyHere: "lo"})

retOne = retTwo; // [ts] Ok because retOne's users do not risk calling unavailable props since retTwo has all of retOne's

retTwo = retOne; // [ts] Type '() => { common: string; }' is not assignable to type '() => { common: string; onlyHere: string; }'. Property 'onlyHere' is missing in type '{ common: string; }' but required in type '{ common: string; onlyHere: string; }'

text

In conclusion, function compatibility is computed oppositely for the parameter list and the return types:

  • subset parameter lists can be assigned to superset ones (reciprocal false).
  • returned object with superset of members can be assigned to a subset (reciprocal false)

IMPORTANT: the type of the variable we are assigning to, does not change after the assignment of a different function shape. So it is still mandatory to provide all parameters of the original variable function type.

Functional Parameter Bivariance

When comparing function parameter lists, assignment succeeds if there is a common denominator between the target and source parameters, it works both ways: CYBER20

Decorators

It is just a functionality that allows you to hook into your code and either extend the functionality of it, or annotate it with metadata. So why would you want to annotate your code with metadata? The answer is you probably don't. There might be some good use cases, but it come more into play if you are building something like a compiler (the Angular compiler for example) and you need to analyse the metadata to do something like dependency injection. So decorators ca provide this metadata, but the can also hook directly into your code and alter the behavior of it. And that part is what is really useful to the average app developer because it allows to write abstractions that is clear and concise. But decorators are almost too good at creating abstractions, so you must be careful not to overuse them. You could be tempted to create a decorator for every little thing, but they should be used for logic that is stable and that needs to be reused frequently throughout the app.

What can you decorate?

  • Class definitions
  • properties
  • methods
  • accessors
  • parameters

For example, lets take an Angular component that we want to prevent from being extended:

// Create a function that will simply make the constructor readonly
function Frozen(constructor: Function) {
  Object.freeze(constructor);
  Object.freeze(constructor.prototype);
}

text

@Frozen
export class IceCreamComponent {

}

// console.log(Object.isFrozen((IceCreamComponent));

class Froyo extends IceCreamComponent {} // [js] Uncaught TypeError: Cannot assign to read only property constructor of object

text

When extending classes with decorators, you need to be careful because the extending class will not recieve the decorator functionalities from the parent.

The most useful decorators are those of properties and methods.

For example let's say we have a flavor property and every time there is such property, we want to add an emoji to the beginning and to the end of it. (Notice that we are calling our decorator with parenthesis @Emoji(). This is because we are using the decorator as a factory, allowing us to pass parameters if needed).

This may sound trivial, but we are actually hooking into the getting and setting of this property which could be very powerful.

We will also create a method decorator to the addTopping method. Note that decorators are composable, so we can stack them on top of each other and they will be executed from top to bottom.

export class IceCreamComponent {
  @Emoji()
  flavor = 'vanilla';

  toppings = [];

  @Confirmable('Are you sure?');
  @Confirmable('Really? Are you super sure?');
  addTopping(topping = 'sprinkles') {
    thisl.toppings.push(topping);
  }
}

text

The key is the name of the property we want to decorate on the target object.

function Emoji() {
  return function(target: Object, key: string | symbol) {
    let val = target[key];
    const getter = () => {
      return val;
    };

    const setter = (next) => {
      console.log('Updating flavor...');
      val = `EMOJI${next}EMOJI`;
    };

    Object.defineProperty(target, key, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });
  }
}

function Confirmable(message: string) {
  return function(target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
    const original = descriptor.value;
    // redefine the method property function by wrapping it with a confirmation
    // but instead of executing the original directly, present a confirm
    // dialog with the message. And if the confirm returns true, then execute
    descriptor.value = function(...args: any[]) {
      const allow = confirm(message);
      let result = null;
      if (allow) {
        // apply the original function with the original arguments
        result = original.apply(this, args);
      }
      return result;
    };
  }
}

text

Above you see that the decorator function should take different parameters depending on what it is supposed to decorate:

  • for property decorators, the function signature is: function(target: Object, key: string | symbol)
  • for method decorators the function signature is: function(target: Object, key: string | symbol, descriptor: PropertyDescriptor), where descriptor is the actual method itself.

Let's try another example with an accessor. Let's say a getter's role is to calculate the price:

export class IceCreamComponent {
  // ...
  basePrice = 5;

  @WithTax(0.15);
  get price() {
    return this.basePrice + 0.25*this.toppings.length;
  }
}

text

One thing you are not allowed with javascript getters, is to pass an argument like price(0.15) //Error, so instead we can use a decorator to add the tax for us.

function WithTax(taxRate) {
  return function(target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
    let original = descriptor.get;
    descriptor.get = function() {
      const result = original.apply(this);
      return (result * (1 + taxRate)).toFixed(2)
    }
    return descriptor;
  }
}

text

Now let's create a final example where we will implement the React useState hook using decorators:

Here is what we want to mimic from React:

import {useState, useEffect} from 'react';

function Example() {
  let [count, setCount] = useState(0);
  // similar to componentDidMount and componentDidUpdate
  useEffect(() => {
    // update the document title using browser API
    document.title = `You pressed ${count} times`;
  });

  return {
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  };
}

text

So using Angular we would need to do the following to get the exact same behavior:

import { Component } from '@angular/core';

// Using an angular built-in decorator to create a component
@Component({
  selector: 'app-hooks',
  template: `
    <p>You clicked [{count}] times</p>
    <button (click)="setCount(count + 1)">
      Click me
    </button>
  `,
});

export class HooksComponent {
  // This is our property decorator
  @UseState(0) count; setCount;

  // and a method decorator that will run an
  // effect either when the component is initialized
  // or when any value changes in the component
  @UseEffect()
  onEffect() {
    document.title = `You clicked ${this.count} times`;
  }
}

// property decorator so has target and key
function useState(seed: any) {
  return function(target: Object, key: string | symbol) {
    target[key] = seed;
    target[`set${key.replace(/^\w/, c => c.toUpperCase())}`] = function(val) { target[key] = val; };
  };
}

function UseEffect() {
  return function(target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
    target.ngOnInit = descriptor.value;
    target.ngAfterViewChecked = descriptor.value;
  }
}

tsc-compiler-options tdd-v-static