TypeScript: Getting Started

This is a small post to get to know how to use all of the most important features in TypeScript

Installation, Init, Configuration and Compile

Globally

npm install -g typescript

Develop

npm i typescript --save-dev

Init

npx tsc --init will generate a file called tsconfig.json where we are going to store all the TypeScript configs for our project, something like this:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    
    "watch": true, // This is not a default one, but it is very handy so TypeScript can regenerate files on the fly while coding
     "sourceMap": true, // Not default option, but very handy for debugging
  }
}

Compile tsc myFile.ts and it will generate a new file called myFile.js from a file called myFile.ts, using tsc command only will generate a js file for each ts file you have in the project if project config are not overwriting default ones.

CLI options To check more options that the TypeScript CLI has, you can check compiler options

Multiple tsconfig.json In bigger projects, we may use multiple tsconfig files, to do so, you can name yout parent file to tsconfig.base.json

Later in your child folder, create a new one childFolder/tsconfig.json and use the option extends

{
  "extends": "../tsconfig.base", // this file is the parent one with default options
   
  // These ones below are overwriting anything from parent
  "compilerOptions": {
    "removeComments": true
  },
  "include": [
    "./**/*"
  ],
  ...
}

Built-in Types

Basic Types: Boolean, Number, String, Array, Enum

Aditional Types: Void, Null, Undefined, Never, Any

Type Annotations

let text : string = "I'm a text"; // declaring variable type string
text = 5; // this will give error, <Type 'number' is not assignable to type 'string'.>

// Example gettting value from a function
let price: number = GetPrice();

Type Inference

In this case, even if we don't annotate the variable as string, typescript will know its type by inference, and it will give error trying to change variable value to number:

let anotherText = "I'm another string";
anotherText = 10; // this will give error

Union Types

It kinda like an or for the type of variable

let someValue: number | string;
someValue = 5; // it works fine
someValue = "text"; // it works fine too

Using the --strictNullChecks Compiler Option

This option set to true, is going help us to avoid bugs and run time errors

let myText: string;
myText = null; // error
myText = undefined; // error

let nullableText: string | null | undefined;
nullableText = null; // ok
nullableText = undefined; // ok

Type Assertions

Sometimes, you know what type of variable you are going to recieve but compailer doesn't, you can use assertion like this:

let value: any = 5;
// Here we make the assertion that value is going to be number type
let fixedString: string = (<number>value).toFixed(2); //5.00

// You can use `as` too, to say value should be a number
let anotherString: string = (value as number).toFixed(2); //5.00

Non-null assertion operator

This way we tell TS that we know that variable is not null

const value = myNonNullObject!.property;

Writing Better Functions with TypeScript

Type annotations to functions

The ? on message? means message parameter is optional, since all function parameters are required by default in typescript. The : string after function(): string means what kind of value are we going to return in that function.

function funFunc(score: number, message?: string): string {
  return "I'm correctly type checked";
}

--noImplicitAny

By default is false, meaning any function paramether can be <any>, but it's helpful to set it true, so, dev has to define the paramether type

// --noImplicitAny : true
function (s){ // it will give error, it needs to have a type diferent to any
    return s + 2
}

Default-Initialized Parameters

// setting greeting parameter to "Good Morning" default value
// in case it is null
function sendGreeting(greeting: string = "Good Morning"): void {
  console.log(greeting);
}

sendGreeting(); // Good Morning
sendGreeting("Good Afternoon!"); // Good Afternoon!

Arrow Functions

const myArrowFunction = (message: string): string => `new ${message}`;

Functions as Types

We can use functions already declared as types for our variables:

let logger: (value: string) => void; // here the type of the variable is a function

const logMessage = (message: string) => console.log(message);

function logError(err: string): void {
  console.error(err);
}

const hasError: boolean = true;
if (hasError) {
  logger = logError; // we assign variable to a function called logError
} else {
  logger = logMessage; // we assign variable to a function called logMessage
}

logger("My String"); // My String

Creating and using custom types

Interfaces vs Clasess

Define a new type | Define a new type Properties (signatures) | Properties (with implementation) Methods (signatures) | Methods (with implementation) Cannot be instantiated | Can be instantiated

Note: Interface are not compiled to JS code, they are used in TSC development code, clases are compiled though

Interfaces

Creating an Interface

An interface define the shape of an object.

// simple interface
interface Employee {
  name: string;
  title: string;
}
// Extending an interface
interface Manager extends Employee {
  department: string;
  numOfEmployees: number;
  scheduleMeeting: (topic: string) => void;
}

Using an Interface

You can use an interface in an object for example as long as that object has all the properties required by interface, if it has more properties that aren't in the interface you can still use it:

interface Employee {
  name: string;
  title: string;
  age?: number; // optional
}

const developer = {
  name: "Jonny Acevedo",
  title: "Senior Front End Developer",
  editor: "VSC", // This one is not defined in the interface, but it still works ok
};

let newEmployee: Employee = developer; // ok

Note: Adding a question mark ? after interface property name, it marks it as optional

Class Members

  • Method implementations
  • Property implementations
  • Accessors (getters and setters)
  • Access modifiers
    • Public (Class members are public by default)
    • Private
    • Protected
class Developer {
  department: string;
  private _title: string;
  get title(): string {
    return this._title;
  }
  set title(newTitle: string) {
    this._title = newTitle.toUpperCase();
  }
  documentRequirements(requirements: string): void {
    console.log(requirements);
  }
}

TSC private vs ECMAScript private fields

private is a technique for restricting access to that member at design time, TSC will stop attempts to access this member from outside the class, but once compailed to JS, that property would be accessible

To make it truly private when compailed to JS, from TypeScript 3.8 you can add # to the class property and it will make it private in the compailed JS as well

class Developer {
  private _title: string;
  #salary: number;
}

Extending a class

class Developer {
  department: string;
  private _title: string;
}

// Extending will inherit Developer properties and add new ones
class WebDeveloper extends Developer {
  favoriteEditor: string;
  customFunction(): void {
    // void
  }
}

let webDev: WebDeveloper = new WebDeveloper();
webDev.department = "Software Engineering";
webDev.favoriteEditor = "VSC";

Implementing an Interface

interface Employee {
  name: string;
  title: string;
  logId: () => string;
}
// use [implements] keyword
class Engineer implements Employee {
  name: string;
  title: string;
  logId() {
    return `${this.name}_${this.title}`;
  }
}

Configuring a project with multiple source files

If we want to ship multiple TSC files in one single JS file, we could use this technique:

tsconfig.json

{
  ...
  "compilerOptions":{
  	...
    "outFile": "../js/app.js"
  },
  "files": [
  	"./app.ts"
  ],
   ...
}

Later, in app.ts

/// <reference path="employee.ts" /> // This is going to reference other TS file
class WevDev implements Employee{
    ...
}

Static Members

Static members are a nice way to add utility or helper methods that are related to the purpose of the class, but aren't dependent on any data that might be stored in instances of the class.

class WebDeveloper extends Developer{
  static jobDescription: string = "Build cool things";
  static logFavoriteProtocol() {
    console.log("HTTPS, of course");
  }
  logJobDescription(): void { 
    // we don't use `this` keyword, we can use class name
    console.log(WebDeveloper.jobDescription);
  }
}
// we don't need to create a new class instance with the `new` keyword
// we can just use class name dot property
WebDeveloper.logFavoriteProtocol();

Constructors

Special type of function executed when new instances of the class are created, handy for perform any special initialization or setup.

class Developer {
  constructor() {
    console.log("Creating a new dev");
  }
}

class WebDeveloper extends Developer {
  //[readonly] value can only be set when variable is declared
  // or inside a constructor
  readonly favoriteEditor: string;

  constructor(editor: string) {
    //[super()] calls the parent class constructor first // Creating a new dev
    super();
    this.favoriteEditor = editor;
  }
}

Initializing class properties with constructor

Long Way:

class Developer {
  name: string;
  age: number;

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

Short Way

class Developer {
  constructor(public name: string, public age: number) {}
}

Creating and consuming Modules

  • Encapsulation
  • Reusability
  • Create higher-level abstractions

Exporting a Declaration

Just like in JS ES6

export interface person {}

export function hireDeveloper(): void {}
// using default
export default class Employee {}

Export Statements

interface Person {}
function hireDeveloper(): void {}
class Employee {}

export { Person, hireDeveloper, Employee as StaffMember };

Importing from a Module

import { Person, hireDeveloper } from "./person";
let human: Person
// this is an example of importing a [default] 
// you can set the alias to any name you want
import Worker from "./person";

// Here we import [StaffMember] class with the alias [CoWorker]
import { StaffMember as CoWorker } from "./person";

// Import an entire module 
import * as HR from "./person";

// File in root
import { myFunction } from "/fileInRoot";

// non-relative imports
import * as  lodash from "lodash";

Type Declaration Files

  • TypeScript wrapper for JavaScript libraries
    • Types for variables, functions, etc
      • Define valid property names
      • Define function parameters and much more
  • Development-time tool
  • Filenames end with .d.ts
  • Available for thousands of libraries

DefinitelyTyped

https://github.com/DefinitelyTyped/DefinitelyTyped

  • Github repository containing thousands of type declaration files
  • Declaration files often maintained independent of related javascript library
  • Source for installation utilities

Installation

One can go to: https://www.typescriptlang.org/dt/search?search=lodash and search for the library desired and you will find instructions to easily install library and its TS types with npm/yarn, in this example, lodash will be like:

npm i lodash
npm i @types/lodash --save-dev

// in your [file.ts], you import it as usual
import * as _ from "lodash"

Thanks for reading, these were my notes for TypeScript, getting started.