typescriptnotes

Initializing a TypeScript Project

## Install TypeScript globally
npm install -g typescript
 
## Compile a TypeScript file
tsc file.ts
 
## Specify the output file
tsc file.ts -out file.js
 
## Compile automatically when a file changes:
tsc file.ts -w
 
## Set up a `tsconfig.json` file:
tsc --init
 
## Compile all TypeScript files and watch for changes:
tsc -w
 
## Compile all TypeScript files in the `src` directory and output to the `dist` directory:
tsc -p src -outDir dist -w

tsconfig.json

{
    "compilerOptions": {
        ...
        /* Modules */
        "target": "es2016", // Change to "ES2015" to compile to ES6
        "rootDir": "./src", // Where to compile from
        "outDir": "./public", // Where to compile to (usually the folder to be deployed to the web server)
 
        /* JavaScript Support */
        "allowJs": true, // Allow JavaScript files to be compiled
        "checkJs": true, // Type check JavaScript files and report errors
 
        /* Emit */
        "sourceMap": true, // Create source map files for emitted JavaScript files (good for debugging)
         "removeComments": true, // Don't emit comments
    },
    "include": ["src"] // Ensure only files in src are compiled
}

Reference Values

In JavaScript, a primitive value is a data type that is not an object and has no methods. Primitives are immutable and cannot be changed. Each primitive value is stored in a unique memory location.

  1. String
  2. Number
  3. Boolean
  4. null
  5. undefined
  6. Symbol
  7. BigInt

Set the type of a variable using a colon : followed by the type.

let name: string = "John";
let age: number = 30;
let isStudent: boolean = false;
let x: null = null;
let y: undefined = undefined;
let sym: symbol = Symbol("key");
let big: bigint = 100n;
 
// Declare a variable without assigning a value
let z: string;

On the other hand, a reference value is an object that is stored in memory and can be accessed by reference. Reference values can be changed.

let person: {
  name: string;
  age: number;
  isStudent: boolean;
} = {
  name: "John",
  age: 30,
  isStudent: false,
};

Arrays

let numbers: number[] = [1, 2, 3, 4, 5];
 
// Use union types to define an array that can contain different types
let values: (string | number)[] = ["John", 30, "Doe", 40];

If you initialise an array with values, it’s not necessary to explicitly state the type, as TypeScript will infer it:

let numbers = [1, 2, 3, 4, 5]; // numbers: number[]

A tuple is an array with a fixed number of elements whose types are known.

let person: [string, number, boolean] = ["John", 30, false];

Functions

// Define a function called circle that takes a diam variable of type number, and returns a string
function circle(diam: number): string {
  return "The circumference is " + Math.PI * diam;
}
 
console.log(circle(10)); // The circumference is 31.41592653589793

Same function with ES6 arrow syntax:

const circle = (diam: number): string => {
  return "The circumference is " + Math.PI * diam;
};
 
console.log(circle(10));

Add a question mark ? to make a parameter optional:

function greet(name: string, greeting?: string): string {
  return greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`;
}

A function that returns void

function log(message: string): void {
  console.log(message);
}

Dynamic Types

let value: any = 5;
value = "John";
value = true;

Type Aliases

type StringOrNumber = string | number;
 
type PersonObject = {
  name: string;
  id: StringOrNumber;
};
 
const person1: PersonObject = {
  name: "John",
  id: 1,
};
 
const person2: PersonObject = {
  name: "Delia",
  id: 2,
};
 
const sayHello = (person: PersonObject) => {
  return "Hi " + person.name;
};
 
const sayGoodbye = (person: PersonObject) => {
  return "Seeya " + person.name;
};

Union Types

let id: string | number;
id = "123";
id = 123;

Extending a Type

type Animal = {
  name: string;
};
 
type Bear = Animal & {
  honey: boolean;
};
 
const bear: Bear = {
  name: "Winnie",
  honey: true,
};

The DOM and Type Casting

With the non-null assertion operator (!) we can tell the compiler explicitly that an expression has value other than null or undefined. This is can be useful when the compiler cannot infer the type with certainty, but we have more information than the compiler.

// Here we are telling TypeScript that we are certain that this anchor tag exists
const link = document.querySelector("a")!;
 
console.log(link.href); // www.freeCodeCamp.org

If we needed to select a DOM element by its class or id, we can use type casting to specify the type of the element.

const form = document.querySelector(".new-item-form") as HTMLFormElement;
const type = document.querySelector("#type") as HTMLSelectElement;
const tofrom = document.querySelector("#tofrom") as HTMLInputElement;

TypeScript also has an event object that can be used to access the properties of an event. We can use type casting to specify the type of the event object.

const form = document.getElementById("signup-form") as HTMLFormElement;
 
form.addEventListener("submit", (e: Event) => {
  e.preventDefault();
  console.log(e.target);
});

Classes

class Person {
  name: string;
  isCool: boolean;
  pets: number;
 
  constructor(n: string, c: boolean, p: number) {
    this.name = n;
    this.isCool = c;
    this.pets = p;
  }
 
  sayHello() {
    return `Hi, my name is ${this.name} and I have ${this.pets} pets`;
  }
}

Access Modifiers

We can also use access modifiers to restrict access to class properties and methods.

class Person {
  readonly name: string; // This property is immutable - it can only be read
  private isCool: boolean; // Can only access or modify from methods within this class
  protected email: string; // Can access or modify from this class and subclasses
  public pets: number; // Can access or modify from anywhere - including outside the class
 
  constructor(n: string, c: boolean, e: string, p: number) {
    this.name = n;
    this.isCool = c;
    this.email = e;
    this.pets = p;
  }
 
  sayMyName() {
    console.log(`Your not Heisenberg, you're ${this.name}`);
  }
}

This can be made more concise by using access modifiers in the constructor:

class Person {
  constructor(
    readonly name: string,
    private isCool: boolean,
    protected email: string,
    public pets: number
  ) {}
 
  sayMyName() {
    console.log(`Your not Heisenberg, you're ${this.name}`);
  }
}

Classes can also be extended:

class Programmer extends Person {
  programmingLanguages: string[];
 
  constructor(
    name: string,
    isCool: boolean,
    email: string,
    pets: number,
    pL: string[]
  ) {
    super(name, isCool, email, pets);
    this.programmingLanguages = pL;
  }
}

Modules

In the tsconfig.json file, change the following options to support modern importing and exporting:

 "target": "es2016",
 "module": "es2015"

(Although, for Node projects you very likely want "module": "CommonJS" – Node doesn’t yet support modern importing/exporting.)

In the HTML file, add the type="module" attribute to the script tag:

<script src="app.js" type="module"></script>

Use import and export to import and export modules:

// src/hello.ts
export function sayHi() {
  console.log("Hello there!");
}
 
// src/script.ts
import { sayHi } from "./hello.js";
 
sayHi(); // Hello there!

Note: akways use the .js extension when importing modules in the browser.

Interfaces

Interfaces define how an object should look. Interfaces won’t get compiled to JavaScript and add bloat to JavaScript, they are only used by the TypeScript compiler to check types.

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

Using the object:

function sayHi(person: Person) {
  console.log(`Hi ${person.name}`);
}
 
sayHi({
  name: "John",
  age: 48,
}); // Hi John

Object types can also be defined anonymously:

function sayHi(person: { name: string; age: number }) {
  console.log(`Hi ${person.name}`);
}

Interfaces with Functions

Interfaces can also define functions:

interface Greet {
  (name: string): string;
}
 
const greet: Greet = (name: string) => `Hello ${name}`;

Interfaces with Classes

interface HasFormatter {
  format(): string;
}
 
class Person implements HasFormatter {
  constructor(public username: string, protected password: string) {}
 
  format() {
    return this.username.toLocaleLowerCase();
  }
}
 
// Must be objects that implement the HasFormatter interface
let person1: HasFormatter;
let person2: HasFormatter;
 
person1 = new Person("Danny", "password123");
person2 = new Person("Jane", "TypeScripter1990");
 
console.log(person1.format()); // danny

Interfaces vs Type Aliases

The key distinction is that type aliases cannot be reopened to add new properties, vs an interface which is always extendable.

interface Animal {
  name: string;
}
 
interface Bear extends Animal {
  honey: boolean;
}
 
const bear: Bear = {
  name: "Winnie",
  honey: true,
};

Adding new fields to an existing interface:

interface Animal {
  name: string;
}
 
// Re-opening the Animal interface to add a new field
interface Animal {
  tail: boolean;
}
 
const dog: Animal = {
  name: "Bruce",
  tail: true,
};

Literal Types

Literal types allow you to specify the exact value a variable can have.

// Union type with a literal type in each position
let favouriteColor: "red" | "blue" | "green" | "yellow";
 
favouriteColor = "blue";
favouriteColor = "crimson"; // ERROR: Type '"crimson"' is not assignable to type '"red" | "blue" | "green" | "yellow"'.

Generics

Generics allow you to have type-safety in components where the arguments and return types are unknown ahead of time.

In TypeScript, generics are used when we want to describe a correspondence between two values.

// <T> is just the convention - e.g. we could use <X> or <A>
function identity<T>(arg: T): T {
  return arg;
}
 
const addID = <T>(obj: T) => {
  let id = Math.floor(Math.random() * 1000);
 
  return { ...obj, id };
};

We can also specify the type of the generic:

const addID = <T extends { name: string }>(obj: T) => {
  let id = Math.floor(Math.random() * 1000);
 
  return { ...obj, id };
};
 
// ERROR: argument should have a name property with string value
let person2 = addID(["Sally", 26]);
 
// Explicitly state what type the argument should be between the angle brackets.
let person1 = addID<{ name: string; age: number }>({ name: "John", age: 40 });

Using Generics Over any

Problem with using any:

function logLength(a: any) {
  console.log(a.length); // No error
  return a;
}
 
let hello = "Hello world";
logLength(hello); // 11
 
let howMany = 8;
logLength(howMany); // undefined (but no TypeScript error)

Using generics:

function logLength<T>(a: T) {
  console.log(a.length); // ERROR: TypeScript isn't certain that `a` is a value with a length property
  return a;
}

Extending Generics

Use generics that extend an interface to ensure that the argument has a length property:

interface hasLength {
  length: number;
}
 
function logLength<T extends hasLength>(a: T) {
  console.log(a.length);
  return a;
}
 
let hello = "Hello world";
logLength(hello); // 11
 
let howMany = 8;
logLength(howMany); // Error: numbers don't have length properties

Write a function where the argument is an array of elements that all have a length property:

interface hasLength {
  length: number;
}
 
function logLengths<T extends hasLength>(a: T[]) {
  a.forEach((element) => {
    console.log(element.length);
  });
}
 
let arr = [
  "This string has a length prop",
  ["This", "arr", "has", "length"],
  { material: "plastic", length: 30 },
];
 
logLengths(arr);
// 29
// 4
// 30

Generics with Interfaces

// The type, T, will be passed in
interface Person<T> {
  name: string;
  age: number;
  documents: T;
}
 
// We have to pass in the type of `documents` - an array of strings in this case
const person1: Person<string[]> = {
  name: "John",
  age: 48,
  documents: ["passport", "bank statement", "visa"],
};
 
// Again, we implement the `Person` interface,
// and pass in the type for documents - in this case a string
const person2: Person<string> = {
  name: "Delia",
  age: 46,
  documents: "passport, P45",
};

Enums

Enums allow us to define or declare a collection of related values, that can be numbers or strings, as a set of named constants.

Enums are useful when we have a set of related constants. For example, instead of using non-descriptive numbers throughout your code, enums make code more readable with descriptive constants.

enum ResourceType {
  BOOK,
  AUTHOR,
  FILM,
  DIRECTOR,
  PERSON,
}
 
console.log(ResourceType.BOOK); // 0
console.log(ResourceType.AUTHOR); // 1
 
// To start from 1
enum ResourceType {
  BOOK = 1,
  AUTHOR,
  FILM,
  DIRECTOR,
  PERSON,
}
 
console.log(ResourceType.BOOK); // 1
console.log(ResourceType.AUTHOR); // 2

Enums with Strings

enum ResourceType {
  BOOK = "BOOK",
  AUTHOR = "AUTHOR",
  FILM = "FILM",
  DIRECTOR = "DIRECTOR",
  PERSON = "PERSON",
}
 
console.log(ResourceType.BOOK); // BOOK
console.log(ResourceType.AUTHOR); // AUTHOR

TypeScript Strict Mode

 // tsconfig.json
 "strict": true

noImplicitAny

With the noImplicitAny option set to true, TypeScript will raise an error when it cannot infer the type of a variable.

// ERROR: Parameter 'a' implicitly has an 'any' type.
function logName(a) {
  console.log(a.name);
}

strictNullChecks

When the strictNullChecks option is false, TypeScript effectively ignores null and undefined. This can lead to unexpected errors at runtime.

With strictNullChecks set to true, null and undefined have their own types, and you’ll get a type error if you assign them to a variable that expects a concrete value (for example, string).

const getSong = () => {
  return "song";
};
 
let whoSangThis: string = getSong();
 
const singles = [
  { song: "touch of grey", artist: "grateful dead" },
  { song: "paint it black", artist: "rolling stones" },
];
 
const single = singles.find((s) => s.song === whoSangThis);
 
console.log(single.artist); // ERROR: Object is possibly 'undefined'.

This forces you to handle the possibility of null or undefined:

if (single) {
  console.log(single.artist);
}

Narrowing in TypeScript

Using the typeof Operator

function addAnother(val: string | number) {
  if (typeof val === "string") {
    // TypeScript treats `val` as a string in this block,
    // so we can use string methods on `val`
    // and TypeScript will not raise an error
    return val.concat(" " + val);
  }
 
  // TypeScript knows `val` is a number here
  return val + val;
}
 
console.log(addAnother("Woooo")); // Woooo Woooo
console.log(addAnother(20)); // 40

We can also give both types a common distinguishing property, with a literal string value:

// All trains must now have a type property equal to 'Train'
interface Train extends Vehicle {
  type: "Train";
  carriages: number;
}
 
// All trains must now have a type property equal to 'Plane'
interface Plane extends Vehicle {
  type: "Plane";
  wingSpan: number;
}
 
type PlaneOrTrain = Plane | Train;

Now TypeScript can narrow down the type based on the type property:

function getVehicleInfo(vehicle: PlaneOrTrain) {
  if (vehicle.type === "Train") {
    console.log(vehicle.carriages);
  } else {
    console.log(vehicle.wingSpan);
  }
}

TypeScript with React

React Props

// src/components/Person.tsx
import React from "react";
 
const Person: React.FC<{
  name: string;
  age: number;
}> = ({ name, age }) => {
  return (
    <div>
      <div>{name}</div>
      <div>{age}</div>
    </div>
  );
};
 
export default Person;

Interfaces can also be used to define the props:

interface Props {
  name: string;
  age: number;
}
 
const Person: React.FC<Props> = ({ name, age }) => {
  return (
    <div>
      <div>{name}</div>
      <div>{age}</div>
    </div>
  );
};

We can then import this component into App.tsx. If we don’t provide the necessary props, TypeScript will give an error.

import React from "react";
import Person from "./components/Person";
 
const App: React.FC = () => {
  return (
    <div>
      <Person name="John" age={48} />
    </div>
  );
};
 
export default App;

React Hooks with TypeScript

useState

We can declare what types a state variable should be by using angle brackets. Below, if we omitted the angle brackets, TypeScript would infer that cash is a number. So, if want to enable it to also be null, we have to specify:

const Person: React.FC<Props> = ({ name, age }) => {
  const [cash, setCash] = useState<number | null>(1);
 
  setCash(null);
 
  return (
    <div>
      <div>{name}</div>
      <div>{age}</div>
    </div>
  );
};

useRef

useRef returns a mutable object that persists for the lifetime of the component. We can tell TypeScript what the ref object should refer to – below we say the prop should be a HTMLInputElement:

const Person: React.FC = () => {
  // Initialise .current property to null
  const inputRef = useRef<HTMLInputElement>(null);
 
  return (
    <div>
      <input type="text" ref={inputRef} />
    </div>
  );
};