TypeScript Fundamentals

What is TypeScript?

  • It's a superset of Javascript
  • Meaning any valid Javascript code is also TypeScript code
  • TypeScript has features that do not exist in Javascript supported by most browser out there:
    • Strong typing
    • Object-oriented features
    • Compile-time errors
    • Great tooling
  • TypeScript needs to be transpiled into Javascript for browser use img

Your First TypeScript Program

  • First of all, we need to install TypeScript:
    • npm install -g typescript
  • Check your version
    • tsc --version (tsc stands for TypeScript compiler)
  • CD to your project folder, create and open file "main.ts" in VS Code
    • code main.ts
  • Type the following codes in editor:
In [1]:
function log(message) {
    console.log(message);
}
var message = 'Hello World';
log(message);
Hello World
Out[1]:
undefined
  • Transpile "main.ts" into Javascript:
    • tsc main.ts
    • this will create a file "main.js"
    • When you run ng serve in Angular, the transpilation takes place and run automatically
  • If you examine files "main.ts" and "main.js", they are the same!
  • You can execute "main.js" via node
    • node main.js

Declaring Variables

In [5]:
function doSomething(){
  for (var i=0; i<5; i++){
    console.log(i);
  }
  
  console.log('Finally: ' + i);
}

doSomething()
0
1
2
3
4
Finally: 5
Out[5]:
undefined

Note the var i declared inside the for block is still available outside the for block !

If we use let instead of var in the for block and save it, we immediately encounter a problem Cannot find name 'i'.

This is the compile error from TypeScript. The beauty of detecting an error early before we deploy/run our app!

The let keyword limit the scope of variable i to the nearest block instead of the nearest function.

If we compile "main.ts" in the terminal tsc main.ts, the compiler show the message Cannot find name 'i' as well but the compiler will still generate the main.js file!

If we examine the "main.js" file, we can find that the change made on variable i let i is restored to var i. This is because by default, TypeScript compiler compiles our TypeScript code to ES5, which is the older version of Javascript. In ES5, there is no let keyword.

JavaScript Versions

  • ES5 (ECMAScript 5): supported by all browsers
  • ES6 (2015)
  • ES2016
  • ES2017

TypeScript compiler reports errors, but still generates valid JavaScript code

Declare variables using the let keyword**

Types

img

We can do the above in JavaScript but not in TypeScript. TypeScript compiler immediately gives you an error type '"a"' is not assignable to type 'number'

If you declare a variable with let without initializing it, the type will be any and even the TypeScript compiler won't complain about it.

In [7]:
let a;
a = 1;
a = true;
a = 'a';
Out[7]:
'a'

If we don't know the value of the variable ahead of time, we can use type annotations

In [9]:
let a: number;
Out[9]:
undefined

The type number includes any integer and floating point numbers

Valid Types

In [12]:
let a: number; // integer & floating point numbers
let b: boolean;
let c: string;
let d: any;
let e: number[]; //array of numbers
let f: any[] = [1, true, 'a', false]; //not a good practice
Out[12]:
undefined

enum

Let's say you're working on a group of related constants, like colors

In [13]:
// Vanilla JS
const ColorRed = 0;
const ColorGreen = 1;
const ColorBlue = 2;
Out[13]:
undefined

In OOP, we can put all related constants in a container so that we don't have to remember the details.

In [15]:
enum Color {Red, Green, Blue};
let backgroundColor = Color.Red;
Out[15]:
undefined

By default, the values of the enum type are 0, 1, 2 respectively

In [16]:
Color.Red
Out[16]:
0

It's a good practice to explicitly specify the value of the enum type because if a new element is added in the future, it won't break parts of the app.

In [18]:
enum Color {Red = 0, Green = 1, Purple = 3, Blue = 2};
Out[18]:
undefined

When we compile the TS file, we can view the implementation of enum in Javascript:

img

Type Assertions

  • By default, if we don't initilize variable at time of declaration, the type will be any
  • We can explicitly set the variable type using Type Assertions
  • With type any, we can't use InterliSense in VS code
  • There are two ways to do type assertions:
In [21]:
// Type Assertion
let message;
message = 'abc';
let endsWithC = (<string>message).endsWith('c');
endsWithC
Out[21]:
true
In [23]:
// Alternative
let alternativeWay = (message as string).endsWith('c');
alternativeWay
Out[23]:
true

Arrow Functions

In [24]:
// Vanilla JS
let log = function (message){
  console.log(message);
}
Out[24]:
undefined
In [25]:
// Arrow Function
let doLog = (message)=>{
  console.log(message);
}
Out[25]:
undefined
In [26]:
// Arrow Function (1 line)
let doLogOneLine = (message) => console.log(message);
Out[26]:
undefined

If the function can be written as a one-line arrow function, we can't skip the () that surrounds the parameter but it's not advised to do so.

If the function has no parameter, we still need the bracket:

In [27]:
// Arrow Function (no params)
let doLogOneLine = () => console.log();
Out[27]:
undefined

Interfaces

  • Use custom type in TypeScript
  • Encapsulate the params in one object and pass to the function

The following function works but it's too verbose:

In [29]:
let drawPoint = (x, y, w, a, b, c) => {
  //...
}
Out[29]:
undefined

A better approach is to wrap every params in one object in the function definition:

In [31]:
let drawPoint = (point) => {
  //...
}
Out[31]:
undefined
In [33]:
drawPoint({
  x:1, 
  y:2
})
Out[33]:
undefined

However, the above approach can't prevent passing in an invalid object

In [34]:
drawPoint({
  name: 'Peter',
  y: 2
})
Out[34]:
undefined

Inline annotation

To validate the object passing in the function, we can use inline annotation:

In [36]:
let drawPoint = (point: {x: number, y: number}) => {
  //...
}
Out[36]:
undefined

However, inline annotation looks too verbose if the object is large with many properties. Also, the same object may be used by other functions as well so it's difficult to maintain the code.

Interface

A better alternative is to use an interface which can make the object reusable and the function definition cleaner. Note that by convention, we capitalize the name of the interface:

In [37]:
interface Point {
  x: number,
  y: number
}
Out[37]:
undefined
In [38]:
let drawPoint = (point: Point) => {
  //...
}
Out[38]:
undefined

However, there is a problem with this approach

Classes

Principle of Cohesion

  • In OOP, cohesion means things that are related should be part of one unit. They should go together

img

In [2]:
interface Point{
  x: number,
  y: number
}

let drawPoint = (point: Point) => {
  //...
}

drawPoint({
  x: 1,
  y: 2
})
Out[2]:
undefined
  • In the above example, the conhesion principle is violated
  • The concept of drawing a point is highly related to the structure of the point. It should NOT be a separate function

Class

  • Group variables (properties) and functions (methods) that are highly related
  • There are differences between properties and fields (more on this later)
  • We can't group the function with interface because interfaces are purely for declarations. They can not include an implementation.
In [6]:
class Point{
  x: number;
  y: number;
  
  
  draw(){
    //...
  }
}
Out[6]:
undefined

Objects

  • An object is an instance of a class
  • When defining an object of a custom type, we need to explicitly allocate memory to it
  • Use the new operator to create an object from a class
  • Metaphor: "Human" is a class, "John", "Mary" are objects
In [9]:
// Instantiate a point object
let point = new Point();
point.x = 1;
point.y = 2;

point.draw();
Out[9]:
undefined

Constructors

  • Every class can have a Constructor, which is basically a method that is called when we created an instance of that class
  • In TypeScript, we can only have one constructor
  • We can set initial values to the Constructor by passing arguments to it or
  • We can create an object without initial value
    • Add ? after the arguments
In [10]:
class Point{
  x: number;
  y: number;
  
  // Constructor with optional parameters
  constructor(x?: number, y?: number){
    this.x = x;
    this.y = y;
  }

  draw(){
    console.log('X: ' + this.x + ' Y: ' + this.y);
  }
}

// Declare object without initial value
let point = new Point();
point.draw();
X: undefined Y: undefined
Out[10]:
undefined

Access Modifiers

  • An access modifier is basically a keyword that we can apply to a member of a class to control it's access from the outside:
    • public
    • private
    • protected
  • By default, all members are public and public and private are the most common
  • We can apply access modifiers on fields, properties and methods
  • If the access modifer of a member of a class is set to private, we can't access the memeber via InterlliSense
In [11]:
class Point{
  private x: number;
  private y: number;
  
  constructor(x?: number, y?: number){
    this.x = x;
    this.y = y;
  }

  draw(){
    console.log('X: ' + this.x + ' Y: ' + this.y);
  }
}

let point = new Point(1, 2);
point.draw();
X: 1 Y: 2
Out[11]:
undefined
  • In the Point class, we have 3 members: 2 fields and 1 method
  • We use private access modifer on the fields x and y
  • Only the draw() method is public and can be accessible from the outside

Access Modifiers in Constructor Parameters

  • In our constructor, we can prefix our parameters with an access modifier
  • TypeScript compiler will generate these fields (x and y) with the exact same name and it will also initialize the fields with the value of this arguments
In [12]:
class Point{
  constructor(private x?: number, private y?: number){
}

  draw(){
    console.log('X: ' + this.x + ' Y: ' + this.y);
  }
}
Out[12]:
undefined

Properties

  • We want user to give the user the ability to set the initial coordinate and be able to change the coordinate later only if they provide a value within a given range
  • If you have a use case like that in your application, you can use what we call the property
  • We define a property using a keywords get or set and then the name of the property
  • We call these Properties Getter and Setter

A property looks like a field from the outside, but internally it's really a method in a class

In [15]:
class Point{
  constructor(private x?: number, private y?: number){
}

  draw(){
    console.log('X: ' + this.x + ' Y: ' + this.y);
  }
  
  // Property X aka 'Getter'
  get X(){
    return this.x;
  }
  
  // Property X aka 'Setter'
  set X(value){
    if (value < 0)
      throw new Error('value cannot be less than 0.');
    
    this.x = value;
  }
}
Out[15]:
undefined

Difference Between Property and Method

  • We can use these properties like fields img

  • We can read X and set X with cleaner syntax:

    • read: let x = point.X;
    • set: point.X = 10;

Camel Casing Notation

  • In JavaScript and TypeScript, we use Camel Casing Notation to name our field i.e. the first letter of the first word is lowercase, and the first letter of the every word after is uppercase
  • To comply with the convention, we rename member "x" to "_x" and change property from "X" to "x"
  • To replace all occurence of "x" with "_x"
    • click "x" and press F2
    • type "_x" and press Enter
  • We do the same thing for "y"
In [17]:
// Camel Casing Notation

class Point{
  constructor(private _x?: number, private _y?: number){
}

  draw(){
    console.log('X: ' + this._x + ' Y: ' + this._y);
  }
  
  // Getter
  get x(){
    return this._x;
  }
  
  // Setter
  set x(value){
    if (value < 0)
      throw new Error('value cannot be less than 0.');
    
    this._x = value;
  }
}

let point = new Point(1, 2);
let x = point.x; // cleaner syntax
point.x = 10;    // cleaner syntax
point.draw();
X: 10 Y: 2
Out[17]:
undefined

Modules

  • The real world application consists of tens or hundreds of files. We don't want to write all the code in one file "main.ts"
In [18]:
// File: main.ts
class Point{
  constructor(private _x?: number, private _y?: number){
}

  draw(){
    console.log('X: ' + this._x + ' Y: ' + this._y);
  }
}

let point = new Point(1, 2);
point.draw();
X: 1 Y: 2
Out[18]:
undefined
  • Ideally, we want to move the definition of this Point class somewhere else as a separate file e.g. "point.ts"
In [19]:
class Point{
  constructor(private _x?: number, private _y?: number){
}

  draw(){
    console.log('X: ' + this._x + ' Y: ' + this._y);
  }
}
Out[19]:
undefined
  • The file "point.ts" contains class definition but it's not accessible from the outside
  • We make the file "point.ts" accessible from the outside by using the export keyword
  • From TypeScript point of view, "point.ts" becomes a module
In [21]:
// point.ts (module)

export class Point{
  constructor(private _x?: number, private _y?: number){
}

  draw(){
    console.log('X: ' + this._x + ' Y: ' + this._y);
  }
}
Out[21]:
[Function: Point]
  • We need to go back to "main.ts" and import this class so we can use it:
    • Point - name of the types
      • If there are multiple types that you want to import, we separate them with a ","
    • ./point - name and path of the module
      • Note the name of the module is "point", not "point.ts"
In [22]:
// main.ts (module)

import { Point } from './point';

let point = new Point(1, 2);
point.draw();
X: 1 Y: 2
Out[22]:
undefined

img

  • These exported types can be classes, functions, simple variable or objects
  • When we have an import or export statements on top of a file, that file is a module from TypeScript's point of view

  • In Angular, we also have the concept of module but it's a little bit different than TypeScript

  • They're not about the organization of code in different files, they are about organization of your application into smaller, function areas
  • When it comes to importing types defined in Angular, we use the library name as the module name e.g. @angular/core
  • More on this in the next section