Typescript

TypeScript is a statically typed superset of JavaScript developed by Microsoft. It adds optional static types, interfaces, and type inference to JavaScript, enabling better tooling, such as code completion and refactoring, and reducing runtime errors. TypeScript code compiles to plain JavaScript, making it compatible with any environment that runs JavaScript.

Getting Started

Bun and Deno are Javascript runtimes that provide a simple way to run a typescript file.

Using Bun:

bun run index.ts

Using Deno:

deno run index.ts

Topics

Compile time vs run time checking

Compile time type checking is performed by the TypeScript compiler, which checks the types of variables and expressions at compile time. This helps catch errors early and provides better tooling support.

function add(a: number, b: number): number {
  return a + b;
}

add(1, 2); // Valid
add(1, '2');
// Compile-time error: Argument of type 'string'
// is not assignable to parameter of type 'number'.

Run time type checking is performed by the JavaScript runtime, which checks the types of variables and expressions at runtime. This allows for more flexibility and dynamic behavior, but may result in runtime errors if the types are not correct.

function add(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new Error('Both arguments must be numbers');
  }
  return a + b;
}

add(1, 2); // Valid
add(1, '2'); // Runtime error: Both arguments must be numbers

Typescript types

Primitive types

let number: number = 1;
let string: string = 'Hello, world!';
let boolean: boolean = true;
let nullValue: null = null;
let undefinedValue: undefined = undefined;
let symbol: symbol = Symbol('mySymbol'); // unique and immutable
let bigInt: bigint = BigInt(10); // BigInt is a new type in ES2020

Object types

//interface
interface User {
  name: string;
  age: number;
}

//class
class User {
  name: string;
  age: number;
}

//enum
enum Color { Red, Green, Blue }

//array types
let arrayOfNumbers: number[] = [1, 2, 3];

//tuple types
let tuple: [number, string] = [1, 'Hello'];

Other types

//any
let any: any = 1;

//object: the parameter's type is an object type
function printCoord(pt: { x: number; y: number }) {
  console.log("The coordinate's x value is " + pt.x);
  console.log("The coordinate's y value is " + pt.y);
}

//unknown
function f2(a: unknown) {
  // Error: Property 'b' does not exist on type 'unknown'.
  a.b();
}

//never
function error(message: string): never {
  throw new Error(message);
}

Type assertions

let a: number = 1;
a = '1'; // error

let b: number = 1;
b = '1' as number; // error

let c: number = 1;
c = '1' as const; // error

Non null assertion operator (!)

let name: string | null = null;
// we use the non-null assertion operator
// to tell the compiler that name will never be null
let nameLength = name!.length;

Type assertion and union types.

type Person = {
  name: string
  age: number
}

type Employee = {
  companyId: number
  role: string
}

var worker: Employee | Person = {
  companyId: 1,
  role: 'worker',
  name: 'John Doe',
  age: 30,
}

console.log((worker as Person).name)
console.log((worker as Employee).companyId)

instanceof and typeof

The typeof operator is used to determine the type of a variable. It returns a string indicating the type of the operand.

let x = 42;
console.log(typeof x); // "number"

let y = 'Hello, World!';
console.log(typeof y); // "string"

let z;
console.log(typeof z); // "undefined"

The instanceof operator is used to check if an object is an instance of a specific class or constructor function.

class Person {
  name: string
  age: number

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

let person = new Person('John', 30)
console.log(person instanceof Person) // true

Literal Types

literal types allow you to specify exact values a variable can hold. They are a subset of more general types (like string, number, or boolean).

//string literal type
let direction: "up" | "down" | "left" | "right";
direction = "up";
direction = "down";
direction = "left";
direction = "right";

//number literal types
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll;
roll = 1;
roll = 2;
roll = 7; //type '7' is not assignable to type 'DiceRoll

//boolean literals types
type YesNo = true | false;
let answer: YesNo;

answer = true; // valid
answer = false; // valid
answer = "yes"; // error: Type '"yes"' is not assignable to type 'YesNo'

Union Types

Union types are a way to represent a value that can be one of several possible types. They are denoted by using the | symbol.

//union type
let a: number | string = 'Hello'

//union type as parameter
function fn(a: number | string) {
  console.log(a)
}

//union type as return type
function fn(): number | string {
  return 'Hello'
}

//union type as property
class Person {
  name: number | string
  age: number | string

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

Intersection Types

Intersection types are used to combine multiple types into a single type. They allow you to define a type that has all the properties of multiple types.

interface Person {
  name: string
  age: number
}

interface Employee {
  companyId: number
  role: string
}

type EmployeeOrPerson = Employee & Person

const worker: EmployeeOrPerson = {
  companyId: 1,
  role: 'worker',
  name: 'John Doe',
  age: 30,
}

Type Aliases

Type aliases are a way to create a new name for an existing type. They are useful when you want to make your code more readable or when you want to create a new type that is similar to an existing one.

type Name = string;
type Age = number;
type User = { name: Name; age: Age };
type fn = (name: Name, age: Age) => void;
type calc = (a: number, b: number, action: (result: number) => void) => void;

Interfaces

Interfaces are a way to define a contract for an object. They specify the properties and methods that an object must have.

//basic interface declaration
interface Person {
  name: string;
  age: number;
}

//optional properties
interface Person {
  name: string;
  age?: number;
}

//readonly properties
interface Person {
  readonly name: string;
  age: number;
}

//method signature
interface Person {
  sayHello(): void;
}

//function types
interface GreetFunction {
  (name: string): string;
}

//indexable types
interface StringArray {
  [index: number]: string;
}

//extending interfaces
interface Person {
  name: string;
  age: number;
}

interface Employee extends Person {
  employeeId: number;
}

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

let counter: Counter = (function (start: number) {
  // function implementation
}) as Counter;
counter.interval = 123;
counter.reset = function () {
  // reset implementation
};

Class implementation

Classes can implement interfaces to enforce a particular structure.

interface Person {
  name: string;
  age: number;
  greet(): string;
}

class Employee implements Person {
  name: string;
  age: number;

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

  greet() {
    return `Hello, my name is ${this.name}.`;
  }
}

Extending Multiple Interfaces

interface Person {
  name: string;
}

interface Contact {
  email: string;
}

interface Employee extends Person, Contact {
  employeeId: number;
}

Interface vs type alias

FeatureInterface 🀝Type Alias 🏷️
Syntaxinterface Person { name: string; }type Person = { name: string; };
UsageObjects, methodsObjects, unions, tuples
Extendingextends keywordIntersection (&)
ClassesImplementable in classesCan’t implement directly
Mergingβœ…βŒ
Unions & IntersectionsβŒβœ…
Index Signaturesβœ…βœ…
Function Typesβœ…βœ…
TuplesβŒβœ…
Computed Propertiesβœ…βœ…
Constraintsextends for constraintsGenerics & intersections
Use CasesBest for object structuresVersatile, complex types

Keyof Operator

The keyof operator is used to get the keys of an object type.

interface User {
  name: string
  age: number
  location: string
}

type UserKeys = keyof User

const nameKey: UserKeys = 'name'
const ageKey: UserKeys = 'age'
const locationKey: UserKeys = 'location'

console.log(nameKey)
console.log(ageKey)
console.log(locationKey)

This is useful for creating function that can only accept valid property names

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user: User = {
  name: 'John',
  age: 30,
  location: 'New York',
}

const userName = getProperty(user, 'name')
const userAge = getProperty(user, 'age')
const userLocation = getProperty(user, 'location')