All Posts

TypeScript basics tutorial

📅 October 7th, 2019

⏱️ 28 min read

Updated: March 31st, 2020

TypeScript is a typed spuerset of JavaScript.

What is TypeScript

TypeScript is an open-source typed superset of JavaScript that compiles to plain JavaScript so that It works in any browser and any OS. In other words:

  • It provides an optional type system to JavaScript
  • It allows you to use planned features from future JavaScript editions

Why TypeScript

You might be asking yourself why would I need to add types to JavaScript?

Large companies have arrived at the conclusion that types enhance code quality and understandability. From my humble experience, this is true for any medium to large scale project because:

  • It's better for the compiler to catch errors than to have errors at runtime that's why types increase agility, especially when doing refactoring.
  • Types are a great form of documentation.

Installation

Install Globally with npm

npm install -g typescript

Create your first TypeScript file

Create a typescript file index.ts. Add the following code to it using your favorite editor (i would suggest VS Code which has built-in support for TypeScript):

interface User {
  firstName: string;
  lastName: string;
}

function userName(user: User) {
  const { firstName, lastName } = user;
  return lastName ? `${firstName} ${lastName}` : firstName;
}

const user = {
  firstName: "Ahmed",
  lastName: "Mokhtar"
};

userName(user);

Compile your code

At the command line, run the TypeScript compiler:

tsc index.ts

The result will be a file index.js with the following code:

function userName(user) {
  var firstName = user.firstName,
    lastName = user.lastName;
  return lastName ? firstName + " " + lastName : firstName;
}
var user = {
  firstName: "Ahmed",
  lastName: "Mokhtar"
};
userName(user);

Configuration

Add the following code to your index.ts:

// this user has only a firstName prop
const user2 = {
  firstName: "Jhon"
};

// when you run the compiler it will throw an error
userName(user2);

Run tsc index.ts. You will notice there is an error in the command line:

index.ts:24:10 - error TS2345: Argument of type '{ firstName: string; }' is not assignable to parameter of type 'User'.
  Property 'lastName' is missing in type '{ firstName: string; }' but required in type 'User'.

24 userName(user2);
            ~~~~~

  index.ts:3:3
    3   lastName: string;
        ~~~~~~~~
    'lastName' is declared here.


Found 1 error.

But if you check index.js you will notice the new code is there:

// this user has only a firstName prop
var user2 = {
  firstName: "Jhon"
};
// when you run the compiler it will throw an error
userName(user2);

By default, TypeScript will emit valid JavaScript the best that it can to help you migrate your JavaScript apps to TypeScript even if there are compilation errors. If you want to prevent this behavior you can use:

Compiler options

  • Delete index.js.
  • Run tsc index.ts --noEmitOnError. Option --noEmitOnError is false by default.
  • index.js will not be emitted unless this error is fixed.

Check the available compiler options.

tsconfig.json

  • Delete index.js if it exists.
  • Create tsconfig.json file:
{
  "compilerOptions": {
    "noEmitOnError": true
  }
}
  • Run tsc with no input files, in which case the compiler searches for the tsconfig.json file.
  • index.js will not be emitted unless this error is fixed.
  • We can fix that error by making the lastName prop optional so that it can be undefined or string:
interface User {
  firstName: string;
  lastName?: string; // this is an optional prop
}
  • Now if you run tsc, index.js will be created as before.
  • We can add some more useful compilerOptions to the config file:
{
  "compilerOptions": {
    // (default es3) Specify ECMAScript target version.
    "target": "es5",
    // (default false) Do not emit outputs if any errors were reported.
    "noEmitOnError": true,
    // (default false) Enable all strict type checking options.
    "strict": true,
    // List of library files to be included in the compilation. (default For --target ES5: DOM,ES5,ScriptHost)
    "lib": ["dom", "dom.iterable", "esnext"]
  }
}

Read more about tsconfig.json.

Type system

TypeScript brings an additional static types layer to JavaScript which only exists when compiling or type-checking source code. Each stored value (variable or property) has a static type that predicts its possible dynamic values. Now let's start with the syntax of the TypeScript type system:

Type annotations

A colon after a variable name starts a type annotation: the type signature after the colon describes what values the variable can have. For example, the following line tells TypeScript that x will only ever store numbers:

let x: number;

if x is initialized as undefined this will violate the static type so that TypeScript doesn't allow you to read x before you assign a value to it.

// error TS2454: Variable 'x' is used before being assigned.
let y = x + 5;
x = 3;
// everything is okay.
let z = x + 6;

Type inference

You don't always have to explicitly specify types. TypeScript can often infer it:

let x = 1;

Then TypeScript infers that x has the static type number.

// error TS2322: Type '"I can't be a string"' is not assignable to type 'number'.
x = "I can't be a string";

Basic types

Boolean

const isAwesome: boolean = true;

Number

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

String

let lang: string = "JavaScript"; // double quotes
lang = "TypeScript"; // single quotes
let fact: string = `I love ${lang}.
${lang} is awesome`; // backtick (template string)

Array

let list: number[] = [1, 2, 3, 4]; // elemType[]
let langs: Array<string> = ["ts", "js", "jsx"]; // Array<elemType>

Tuple

// Declare a tuple type
let tuple: [string, number];
// Initialize it
tuple = ["hello", 10]; // OK
// Initialize it incorrectly
tuple = [10, "hello"]; // Error
tuple[0].substring(1); // OK
tuple[1].substring(1); // Error, Property 'substring' does not exist on type 'number'.
tuple[3] = "world"; // Error, Type '"world"' is not assignable to type 'undefined'.
tuple[5].toString(); // Error, Object is possibly 'undefined'.

Enum

Enums or enumerations are a new data type supported in TypeScript. In simple words, enums allow us to declare a set of named constants i.e. a collection of related values that can be numeric or string values. I will try to give some real-world examples of how we can convert some JavaScript code to use enum:

// this is what we would normally do in JavaScript
// it's a valid TypeScript code as well
const State = {
  UNDETERMINED: 0,
  FAILED: 1,
  BEGAN: 2,
  CANCELLED: 3,
  ACTIVE: 4,
  END: 5
};

Numeric enums are number-based enums i.e. they store string values as numbers:

// by default enums will have value starting from 0 and incrementing
// but you can give them a value of your choice
enum State {
  UNDETERMINED, // 0
  FAILED, // 1
  BEGAN, // 2
  CANCELLED, // 3
  ACTIVE, // 4
  END // 5
}

console.log(typeof State.ACTIVE, State.ACTIVE); // number 4
// Numeric enums supports reverse mapping
console.log(typeof State[0], State[0]); // string FAILED

Another example if you are familiar with redux you may do something like that:

const actionTypes = {
  ADD_TODO: "ADD_TODO",
  REMOVE_TODO: "REMOVE_TODO",
  UPDATE_TODO: "UPDATE_TODO",
  DELETE_TODO: "DELETE_TODO",
  TOGGLE_TODO: "TOGGLE_TODO"
};

String enums are similar to numeric enums, except that the enum values are initialized with string values rather than numeric values and they don't support reverse mapping.

enum ActionTypes {
  // lowerCase to showcase that it doesn't support reverse mapping
  ADD_TODO = "add_todo",
  REMOVE_TODO = "REMOVE_TODO",
  UPDATE_TODO = "UPDATE_TODO",
  DELETE_TODO = "DELETE_TODO"
}
// { ADD_TODO: 'add_todo',
//   REMOVE_TODO: 'REMOVE_TODO',
//   UPDATE_TODO: 'UPDATE_TODO',
//   DELETE_TODO: 'DELETE_TODO' }
console.log(ActionTypes);
// string add_todo
console.log(typeof ActionTypes.ADD_TODO, ActionTypes.ADD_TODO);
// error TS2551: Property 'add_todo' does not exist on type 'typeof ActionTypes'. Did you mean 'ADD_TODO'?
console.log(ActionTypes["add_todo"]);

Then you can create your action creators like that:

const addTodo = (todo: string) => ({
  type: ActionTypes.ADD_TODO,
  todo
});

Or with stronger typing:

interface AddTodoAction {
  type: ActionTypes.ADD_TODO;
  todo: string;
}

const addTodo = (todo: string): AddTodoAction => ({
  type: ActionTypes.ADD_TODO,
  todo
});

Any

It gives us an escape hatch from the type system when we want to opt-out of type checking and let the values pass through compile-time checks.

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

Void

void the absence of having any type at all. Use it to signify that a function does not have a return type(returns undefined explicitly or implicitly):

function f1(): void {
  return undefined;
} // OK
function f2(): void {} // OK
function f3(): void {
  return "abc";
} // Error: Type '"abc"' is not assignable to type 'void'.

Null and Undefined

In TypeScript, both undefined and null literals actually have their own types named undefined and null respectively. They can be assigned to any other type:

let num: number;
let str: string;
// These literals can be assigned to anything
num = null;
str = undefined;

However, when using the --strictNullChecks flag which is true when the --strict flag is used as well, null and undefined are only assignable to any and their respective types (the one exception being that undefined is also assignable to void):

// when the strict compiler option is used it enables the strictNullCheck option
let num: number;
let str: string;
num = null; // Error: Type 'null' is not assignable to type 'number'.
str = undefined; // Error: Type 'undefined' is not assignable to type 'string'.

When --strictNullChecks is true, you can use union type which we will discuss in another article as following:

let num: number | null;
let str: string | undefined | null;
num = null;
str = undefined;
str = null;

Never

The never type represents the type of values that never occur:

// Function returning never must have an unreachable endpoint
function error(message: string): never {
  throw new Error(message);
}

// Inferred return type is never
function fail() {
  return error("Something failed");
}

// Function returning never must have an unreachable endpoint
function infiniteLoop(): never {
  while (true) {}
}

Object

object is a type that represents the non-primitive type, i.e. anything that is not number, string, boolean, symbol, null, or undefined. With the object type, APIs like Object.create can be better represented. For example:

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

Type assertions

TypeScript's type assertion is purely you telling the compiler that you know about the types better than it does, and that it should not second guess you. Usually, this will happen when you know the type of some entity could be more specific than its current type. Type assertions have two forms. One is the “angle-bracket” syntax (can't be used with JSX):

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

And the other is the as-syntax:

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

Functions

Functions are the core building block of a composable system. Let's see how we can type them:

// Named function with parameter type and infer the return type
function sum(x: number, y: number) {
  return x + y;
}

// Anonymous function parameter type and return type
let sum2 = function(x: number, y: number): number {
  return x + y;
};

// Frrow function with parameter type and return type
let sum3 = (x: number, y: number): number => x + y;

// function type
let sum4: (x: number, y: number) => number = (x, y) => x + y;
let sum5: (baseValue: number, increment: number) => number = function(
  x: number,
  y: number
): number {
  return x + y;
};

In TypeScript the number of arguments given to a function has to match the number of parameters the function expects:

function buildName(firstName: string, lastName: string) {
  return firstName + " " + lastName;
}

let result1 = buildName("Bob"); // Error: Expected 2 arguments, but got 1.
let result2 = buildName("Bob", "Adams", "Sr."); // Expected 2 arguments, but got 3.
let result3 = buildName("Bob", "Adams"); // OK

Optional parameters

In JavaScript, every parameter is optional, and users may leave them off as they see fit. When they do, their value is undefined. We can get this functionality in TypeScript by adding a ? to the end of parameters we want to be optional. For example, let’s say we want the last name parameter from above to be optional:

function buildName(firstName: string, lastName?: string) {
  return lastName ? firstName + " " + lastName : firstName;
}

let result1 = buildName("Bob"); // OK
let result2 = buildName("Bob", "Adams", "Sr."); // Error: Expected 1-2 arguments, but got 3.
let result3 = buildName("Bob", "Adams"); // OK

Any optional parameters must follow the required parameters. Had we wanted to make the first name optional, rather than the last name, we would need to change the order of parameters in the function, putting the first name last in the list.

// Error: A required parameter cannot follow an optional parameter.
function buildName(firstName?: string, lastName: string) {
  return lastName ? firstName + " " + lastName : firstName;
}

Default parameters

function greetings(name: string = "friend"): string {
  return `Hello ${name}!`;
}

const res1 = greetings(); // return: "Hello friend!"
const res2 = greetings("Ahmed"); // return: "Hello Ahmed!"

Rest parameters

Rest parameters are treated as a boundless number of optional parameters. When passing arguments for a rest parameter, you can use as many as you want, you can even pass none. The compiler will build an array of the arguments passed in with the name given after the ellipsis (...), allowing you to use it in your function:

const buildFullName = (firstName: string, ...restOfName: string[]) =>
  firstName + " " + restOfName.join(" ");

// employeeName will be "Ahmed Mokhtar Mohammed Saad"
const employeeName = buildFullName("Ahmed", "Mokhtar", "Mohammed", "Saad");

Overloading

TypeScript provides the concept of function overloading. You can have multiple functions with the same name but different parameter types and return type. However, the number of parameters should be the same:

function add(a: string, b: string): string;
function add(a: number, b: number): number;
function add(a: any, b: any) {
  return a + b;
}
add("Hello ", "friend"); // returns "Hello friend"
add(1, 2); // returns 3
add("Hello", 2); // Error: Argument of type '"Hello"' is not assignable to parameter of type 'number'.

Interfaces

Interfaces have zero runtime JS impact because the TypeScript compiler does not convert interfaces to JavaScript. It uses interfaces for type checking. One of TypeScript’s core principles is that type checking focuses on the shape that values have. This is also known as "duck typing" or "structural subtyping". Interfaces are capable of describing the wide range of shapes that JavaScript objects can take like objects, functions, arrays:

interface Person {
  id: string;
  name: string;
  gender: string;
  age: number;
}

function printPerson(person: Person) {
  const { name, gender, age } = person;
  console.log(`name: ${name}, gender: ${gender}, age: ${age}`);
}

const person: Person = {
  id: "123456789",
  name: "Ahmed",
  gender: "male",
  age: 27
};

printPerson(person); // name: Ahmed, gender: male, age: 27

Optional properties

As we have seen in a previous example we can make a property optional using ?:

interface Person {
  id: string;
  name: string;
  gender?: string; // Optional
  age: number;
}

function printPerson(person: Person) {
  const { name, gender, age } = person;
  console.log(
    `name: ${name}, gender: ${gender ? gender : "unknown"}, age: ${age}`
  );
}

const person2: Person = {
  id: "987654321",
  name: "Jhon",
  age: 50
};

printPerson(person2);

Readonly properties

You can mark a property as readonly so that it can only be assigned a value when an object is first created:

interface Person {
  readonly id: string;
  name: string;
  gender?: string;
  age: number;
}

const person: Person = {
  id: "123456789",
  name: "Ahmed",
  gender: "male",
  age: 27
};

person.id = "can't be changed"; // Error: Cannot assign to 'id' because it is a read-only property.

Function Types

To describe a function type with an interface, we give the interface a call signature:

interface UpperCaseFirstCharFunc {
  (str: string): string;
}

const upperCaseFirstChar: UpperCaseFirstCharFunc = str => {
  return str.charAt(0).toUpperCase() + str.substring(1);
};

upperCaseFirstChar("ahmed"); // returns: "Ahmed"

Indexable Types

we can also describe types that we can “index into” like a[10], or ageMap["daniel"]:

interface StringArray {
  [index: number]: string;
}
const myArray: StringArray = ["Ahmed", "Jhon"];
const myStr: string = myArray[0];

interface NumberDictionary {
  [index: string]: number;
  length: number; // ok, length is a number
  name: string; // error, Property 'name' of type 'string' is not assignable to string index type 'number'.
}

interface NumberOrStringDictionary {
  [index: string]: number | string;
  length: number; // ok, length is a number
  name: string; // ok, name is a string
}

interface ReadonlyStringArray {
  readonly [index: number]: string;
}
const myArr: ReadonlyStringArray = ["Alice", "Bob"];
myArr[2] = "Mallory"; // Error: Index signature in type 'ReadonlyStringArray' only permits reading.

Class Types

Implementing an interface

Interfaces describe the public side of the class, rather than both the public and private side. This prohibits you from using them to check that a class also has particular types for the private side of the class instance.

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

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  setTime(d: Date) {
    this.currentTime = d;
  }
  constructor(h: number, m: number) {}
}
Difference between the static and instance sides of classes

When a class implements an interface, only the instance side of the class is checked. Since the constructor sits in the static side, it is not included in this check so that if you create an interface with a construct signature and try to create a class that implements this interface you get an error:

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

// Error: Class 'Clock' incorrectly implements interface 'ClockConstructor'.
// Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): void'.
class Clock implements ClockConstructor {
  currentTime: Date = new Date();
  constructor(h: number, m: number) {}
}

To check both the static side and instance side you can use class expressions:

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

interface ClockInterface {
  tick(): void;
}

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

Extending Interfaces

interface Person {
  name: string;
}

interface Employee extends Person {
  salary: number;
}

const employee: Employee = {
  name: "Ahmed",
  salary: 1000000
};

Classes

Example:

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

Inheritance

class Animal {
  move(distanceInMeters: number = 0) {
    console.log(`Animal moved ${distanceInMeters}m.`);
  }
}

class Dog extends Animal {
  bark() {
    console.log("Woof! Woof!");
  }
}

const dog = new Dog();
dog.bark(); // Woof! Woof!
dog.move(10); // Animal moved 10m.
dog.bark(); // Woof! Woof!

A more complex example:

class Animal {
  name: string;
  constructor(theName: string) {
    this.name = theName;
  }
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

class Snake extends Animal {
  // Constructors for derived classes must contain a 'super' call which will execute the constructor of the base class and allows you to access a property on this in a constructor body
  constructor(name: string) {
    super(name);
  }
  // overriding the move method of the base class Animal
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

class Horse extends Animal {
  constructor(name: string) {
    super(name);
  }
  move(distanceInMeters = 45) {
    console.log("Galloping...");
    super.move(distanceInMeters);
  }
}

let sam = new Snake("Sammy the Python");
// even though tom is declared as an Animal, since its value is a Horse, calling tom.move(34) will call the overriding method in Horse:
let tom: Animal = new Horse("Tommy the Palomino");

// Slithering...
// Sammy the Python moved 5m.
sam.move();
// Galloping...
// Tommy the Palomino moved 34m.
tom.move(34);

Access Modifiers

In Object-Oriented Programming, access modifiers are being used for restricting or allowing to access the variables of a class from outside. There are 3 types of access modifiers:

  • Public: Allows access from outside of a class.
  • Private: Doesn’t allow access from outside of a class.
  • Protected: Allows access only within a class and its derived classes.

All members of a class are public by default:

// this is the same as not using public at all
class Animal {
  public name: string;
  public constructor(theName: string) {
    this.name = theName;
  }
  public move(distanceInMeters: number) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

private:

class Animal {
  private name: string;
  constructor(theName: string) {
    this.name = theName;
  }
}

new Animal("Cat").name; // Error: Property 'name' is private and only accessible within class 'Animal'.

protected:

class Person {
  protected name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Employee extends Person {
  private department: string;

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

  public getElevatorPitch() {
    return `Hello, my name is ${this.name} and I work in ${this.department}.`;
  }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // Error: Property 'name' is protected and only accessible within class 'Person' and its subclasses.

Readonly modifier

Readonly properties must be initialized at their declaration or in the constructor:

class Octopus {
  readonly name: string;
  readonly numberOfLegs: number = 8;
  constructor(theName: string) {
    this.name = theName;
  }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // Error: Cannot assign to 'name' because it is a read-only property.
Parameter properties

Parameter properties let you create and initialize a member in one place:

class Octopus {
  readonly numberOfLegs: number = 8;
  constructor(readonly name: string) {}
}

Accessors

TypeScript supports getters/setters as a way of intercepting accesses to a member of an object:

const fullNameMaxLength = 10;

class Employee {
  constructor(private _fullName: string) {}

  get fullName(): string {
    return this._fullName;
  }

  set fullName(newName: string) {
    if (newName && newName.length > fullNameMaxLength) {
      console.warn("fullName has a max length of " + fullNameMaxLength);
      return;
    }

    this._fullName = newName;
  }
}

let employee = new Employee("A Mokhtar");
employee.fullName = "Ahmed Mokhtar"; // more than 10 won't work
// fullName has a max length of 10
console.log(employee.fullName); // A Mokhtar
employee.fullName = "Will Smith"; // OK
console.log(employee.fullName); // Will Smith

Static Properties

Static members are visible on the class itself rather than on the instances. we use static keyword to mark them:

class Circle {
  static pi: number = 3.14; // static pi

  pi = 3; // public pi

  static calculateArea(radius: number) {
    // static member can be accessed here as this.pi
    return this.pi * radius * radius;
  }

  calculateCircumference(radius: number): number {
    // static member can be accessed here as Circle.pi
    return 2 * Circle.pi * radius;
  }
}

// static members can be accessed from the class itself
Circle.pi; // returns 3.14
Circle.calculateArea(5); // returns 78.5

// non-static field can be accessed from an instance
let circleObj = new Circle();
circleObj.pi; // returns 3

Abstract Classes

Abstract classes are mainly for inheritance where other classes may derive from them. We cannot create an instance of an abstract class:

abstract class Department {
  constructor(public name: string) {}

  printName(): void {
    console.log("Department name: " + this.name);
  }

  abstract printMeeting(): void; // must be implemented in derived classes
}

class AccountingDepartment extends Department {
  constructor() {
    super("Accounting and Auditing"); // constructors in derived classes must call super()
  }

  printMeeting(): void {
    console.log("The Accounting Department meets each Monday at 10 am.");
  }

  generateReports(): void {
    console.log("Generating accounting reports...");
  }
}

let department: Department; // ok to create a reference to an abstract type
department = new Department(); // Error: Cannot create an instance of an abstract class.
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // Error: Property 'generateReports' does not exist on type 'Department'.

Advanced Techniques

Constructor functions

When you declare a class in TypeScript, you are actually creating multiple declarations at the same time:

  • The type of the instance of the class.
  • The constructor function. This is the function that is called when we new up instances of the class.
class Greeter {
  static standardGreeting = "Hello, there";
  greeting: string;
  greet() {
    if (this.greeting) {
      return "Hello, " + this.greeting;
    } else {
      return Greeter.standardGreeting;
    }
  }
}

let greeter1: Greeter; // type of instance
greeter1 = new Greeter(); // create an instance
console.log(greeter1.greet()); // use the instance

// greeterMaker is assigned the class itself or (its constructor function)
// typeof Greeter is the type of the constructor function
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!"; // static member of Greeter

// use greeterMaker which is equal to the class Greeter itself (its constructor function) to create an instance
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());
Using a class as an interface

Because classes create types representing instances of the class, you can use them in the same places you would be able to use interfaces.

class Point {
  x: number;
  y: number;
}

interface Point3d extends Point {
  z: number;
}

let point3d: Point3d = { x: 1, y: 2, z: 3 };

If the --strictNullChecks flag is true (it's true also when the --strict flag is true) you will see this error: Property 'x' has no initializer and is not definitely assigned in the constructor. which can be fixed in 3 ways depending on your need:

  • Using the type assertion operator ! which tells the compiler don't worry this value will not be null or undefined.
  • Using default values
  • Using the optional operator ? to tell the compiler that this value may be undefined.
// type assertion operator !
class Point {
  x!: number;
  y!: number;
}

// default values
class Point {
  x: number = 0;
  y: number = 0;
}

// optional operator ?
class Point {
  x?: number;
  y?: number;
}

Generics

Generics offer a way to create reusable components. They provide a way to make components work with a variety of data types and not to be restricted to one data type. This example will show why you might need generics:

function getArray(items: any[]): any[] {
  return new Array().concat(items);
}

let myNumArr = getArray([100, 200, 300]);
let myStrArr = getArray(["Hello", "World"]);

myNumArr.push(400); // OK
myStrArr.push("Hello TypeScript"); // OK

// here we added a string to numbers array which isn't the desired behavior
myNumArr.push("Hi"); // OK
// here we added a number to strings array which isn't the desired behavior
myStrArr.push(500); // OK

console.log(myNumArr); // [100, 200, 300, 400, "Hi"]
console.log(myStrArr); // ["Hello", "World", "Hello TypeScript", 500]

We will use a type variable, a special kind of variable that works on types rather than values. We will add the type variable T to the getArray function. This T allows us to capture the type the user provides (e.g. number) so that we can use that information later and we will also use T again as the return type.:

function getArray<T>(items: T[]): T[] {
  return new Array<T>().concat(items);
}

let myNumArr = getArray<number>([100, 200, 300]); // number will replace T so type of argument and return type will be number[]
let myStrArr = getArray<string>(["Hello", "World"]); // string will replace T so type of argument and return type will be string[]

myNumArr.push(400); // OK
myStrArr.push("Hello TypeScript"); // OK

myNumArr.push("Hi"); // Error: Argument of type '"Hi"' is not assignable to parameter of type 'number'.
myStrArr.push(500); // Error: Argument of type '500' is not assignable to parameter of type 'string'.

// we can also use inference
let numArr = getArray([100, 200, 300]); // type of numArr is number[]

Generic types

We will show examples of generic functions and generic interfaces:

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

let myIdentity: <T>(arg: T) => T = identity;

// different name of generic type parameter can be used so long as
// the number of type variables and how the type variables are used line up.
let myIdentity1: <U>(arg: U) => U = identity;

// we can also use an interface to represent the generic function
interface GenericIdentityFn {
  <T>(arg: T): T;
}

let myIdentity2: GenericIdentityFn = identity;

// we can also make the interface itself generic which will make the type
// parameter available to all members of the interface
interface GenericIdentityFn2<T> {
  (arg: T): T;
}

let myIdentity3: GenericIdentityFn2<number> = identity;

Generic Classes

Generic classes are only generic over their instance side rather than their static side, so when working with classes, static members can not use the class’s type parameter.

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

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

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) {
  return x + y;
};

console.log(stringNumeric.add(stringNumeric.zeroValue, "test")); // test

Generic Constraints

The compiler could not prove that every type the user will provide had a .length property, so it warns us that we can’t make this assumption.

function loggingIdentity<T>(arg: T): T {
  console.log(arg.length); // Error: Property 'length' does not exist on type 'T'.
  return arg;
}

To tell the compiler that we will use a type that has .length property, we’ll create an interface that describes our constraint and make the type parameter extends it:

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // Now we know it has a .length property, so no more error
  return arg;
}

loggingIdentity<number>(3); // Type 'number' does not satisfy the constraint 'Lengthwise'.
loggingIdentity<string>("3"); // OK
loggingIdentity({ length: 10, value: 3 }); // OK
Using Type Parameters in Generic Constraints

You can declare a type parameter that is constrained by another type parameter:

// K parameter must be a property in T
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // OK
getProperty(x, "m"); // error: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
Using Class Types in Generics

Creating factories using generics:

function create<T>(c: { new (): T }): T {
  return new c();
}

Example:

class BeeKeeper {
  hasMask!: boolean;
}

class ZooKeeper {
  nametag!: string;
}

class Animal {
  numLegs!: number;
}

class Bee extends Animal {
  keeper!: BeeKeeper;
}

class Lion extends Animal {
  keeper!: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}

createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!

Conclusion

TypeScript is a great addition to any developer's skills. It is very particular about keeping the barrier to entry as low as possible. It provides an optional type system for JavaScript and allows you to use planned features from future JavaScript editions. It makes it easier to develop and maintain larger projects. I will try to write about TypeScript advanced types and utility types because you will definitely need them and also write about TypeScript usage with React and Redux.

Please, feel free to leave a comment.

I hope you find this useful.

Peace.


Be the First to Leave a Comment!

Leave a Comment 💬