Skip to content

TypeScript

TypeScript Logo

Table of Contents

Part 1: Getting Started

1. Installation

Install TypeScript with npm (Node.js package manager). Install it globally so you can run tsc from anywhere.

npm install -g typescript

2. The Compiler-(tsc)

Here's a simple TypeScript file to get you started.

greeter.ts

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

const user = "World";
console.log(greet(user));

To compile this, run the TypeScript Compiler (tsc):

tsc greeter.ts

This creates greeter.js plain JavaScript you can run in a browser or with Node.js.

greeter.js (Compiled Output)

function greet(name) {
    return "Hello, " + name + "!";
}
var user = "World";
console.log(greet(user));

3. The tsconfig.json File

For real projects, use tsconfig.json to configure the compiler. Generate one by running:

tsc --init

This creates a file with many options and helpful comments. Here are the most important ones to know:

{
  "compilerOptions": {
    /* Target JavaScript Version */
    "target": "ES2020", // Compile to modern JavaScript

    /* Module System */
    "module": "commonjs", // For Node.js, or "ESNext" for browsers

    /* Strictness */
    "strict": true, // This is the most important setting! It enables all strict type checking options.

    /* Output */
    "outDir": "./dist", // Where to put the compiled .js files
    "rootDir": "./src", // Where to find the source .ts files

    "esModuleInterop": true, // Allows better compatibility with CommonJS modules
    "forceConsistentCasingInFileNames": true // Prevents case related errors
  }
}

With a tsconfig.json in place, you can just run tsc in your project root, and it will compile all your .ts files according to the rules you've set.

Part 2: The Core of TypeScript

4. Basic Types: string, number, boolean

You can explicitly add type annotations to your variables. If you don't, TypeScript will try to infer the type.

let framework: string = "TypeScript";
let version: number = 4.9;
let isAwesome: boolean = true;

// Type Inference
let inferredString = "I am a string"; // TypeScript knows this is a string
// inferredString = 123; // Error: Type 'number' is not assignable to type 'string'.

5. Typing Arrays and Tuples

  • Arrays: You can type arrays in two ways.
  • Tuples: Fixed length, fixed type arrays.
// An array of numbers
let numbers: number[] = [1, 2, 3];
let moreNumbers: Array<number> = [4, 5, 6];

// A tuple: an array with a specific sequence of types
let user: [number, string];
user = [1, "Alice"];
// user = ["Bob", 2]; // Error!

6. The any, unknown, and never Types

  • any: The "escape hatch." A variable of type any can be anything. It disables all type checking for that variable. Using any defeats the purpose of TypeScript and should be avoided whenever possible.
  • unknown: The type safe alternative to any. A variable of type unknown can be anything, but you must perform type checks before you can use it.
  • never: Represents a value that will never occur. Used for functions that always throw an error or have an infinite loop.
let notSure: unknown = 4;

// console.log(notSure.toFixed(2)); // Error: Object is of type 'unknown'.

if (typeof notSure === "number") {
  // This is fine, we've checked the type.
  console.log(notSure.toFixed(2)); // "4.00"
}

7. The null, undefined, and void Types

  • null and undefined: These are types with only one value: null and undefined, respectively. With "strictNullChecks": true in tsconfig.json (part of "strict": true), TypeScript will force you to handle cases where a value could be null or undefined.
  • void: The return type of a function that does not return a value.
function logMessage(message: string): void {
  console.log(message);
}

Part 3: Shaping Your Data

8. Interfaces: Defining Object Shapes

An interface is a way to define a "contract" for an object's shape. It specifies the property names and their types.

interface User {
  id: number;
  username: string;
  isActive: boolean;
  readonly registrationDate: Date; // Property cannot be changed after creation
  bio?: string; // The `?` makes this property optional
}

const user1: User = {
  id: 1,
  username: "alice",
  isActive: true,
  registrationDate: new Date(),
};

// user1.registrationDate = new Date(); // Error: Cannot assign to 'registrationDate' because it is a read only property.

9. Type Aliases: Naming Your Types

A type alias lets you create a new name for a type. It's useful for simplifying complex types like unions.

type UserID = number | string;

function getUser(id: UserID) {
  // ...
}

getUser(123);       // OK
getUser("abc-123"); // OK

interface vs. type: For defining object shapes, they are very similar. A key difference is that interfaces can be extended by other interfaces, while types cannot. A common convention is to use interface for object shapes and type for all other custom types.

10. Union Types-(|)

A union type allows a variable to be one of several types.

function printId(id: number | string) {
  if (typeof id === "string") {
    // Here, TypeScript knows `id` is a string
    console.log(id.toUpperCase());
  } else {
    // Here, TypeScript knows `id` is a number
    console.log(id);
  }
}

11. Intersection Types-(&)

An intersection type combines multiple types into one.

interface Loggable {
  log(): void;
}

interface Serializable {
  serialize(): string;
}

// The variable `myObject` must have both a `log` and a `serialize` method.
type LoggableAndSerializable = Loggable & Serializable;

Part 4: Functions in TypeScript

12. Typing Parameters and Return Values

TypeScript allows you to be explicit about what your functions expect and what they will return.

// This function takes two numbers and is explicitly typed to return a number.
function add(a: number, b: number): number {
  return a + b;
}

// TypeScript can also infer the return type, but being explicit is often clearer.
function subtract(a: number, b: number) { // Return type is inferred as `number`
  return a b;
}

13. Optional and Default Parameters

  • Add a ? after a parameter name to make it optional.
  • Provide a default value to a parameter to make it optional and give it a value if none is provided.
function greet(name: string, greeting?: string) {
  if (greeting) {
    return `${greeting}, ${name}!`;
  } else {
    return `Hello, ${name}!`;
  }
}

function power(base: number, exponent: number = 2): number {
  return Math.pow(base, exponent);
}

console.log(power(3)); // 9 (exponent defaults to 2)
console.log(power(3, 3)); // 27

Part 5: Advanced Types & Generics

14. Generics-(<T>): Creating Reusable Components

Generics allow you to create components (like functions or classes) that can work with a variety of types rather than a single one. This is a core feature for creating reusable and type safe code.

// Without generics, you might have to use `any`, which is not type safe.
function identity_any(arg: any): any {
  return arg;
}

// With generics, we create a type variable `T`.
// This function takes an argument of type `T` and returns a value of type `T`.
function identity<T>(arg: T): T {
  return arg;
}

// TypeScript can infer the type of T
let output1 = identity("myString"); // output1 is of type `string`

// Or you can set it explicitly
let output2 = identity<number>(123); // output2 is of type `number`

15. Enums: Named Constants

Enums allow you to define a set of named constants. They can make your code more readable.

enum LogLevel {
  INFO,
  WARN,
  ERROR,
  DEBUG
}

function log(level: LogLevel, message: string) {
  // ...
}

log(LogLevel.INFO, "User logged in.");

Part 6: Object Oriented Programming

TypeScript fully supports object oriented programming with classes, just like Java or C#.

16. Classes, Fields, and Methods

class Player {
  // Fields with types
  username: string;
  health: number;

  // Constructor
  constructor(username: string) {
    this.username = username;
    this.health = 100;
  }

  // Method
  takeDamage(amount: number): void {
    this.health -= amount;
    console.log(`${this.username} took ${amount} damage. Health is now ${this.health}.`);
  }
}

const player1 = new Player("Gopher");
player1.takeDamage(10);

17. Access Modifiers and readonly

  • public: (Default) Can be accessed from anywhere.
  • private: Can only be accessed from within the same class.
  • protected: Can be accessed from within the class and by subclasses.
  • readonly: Can only be set in the constructor.
class User {
  public readonly id: number;
  private secret: string;

  constructor(id: number) {
    this.id = id;
    this.secret = "my secret";
  }
}

18. Inheritance-(extends) and Interfaces (implements)

interface IAttack {
  attack(target: Character): void;
}

class Character {
  constructor(public name: string, public health: number) {}
}

class Hero extends Character implements IAttack {
  constructor(name: string) {
    super(name, 100); // Call the parent constructor
  }

  attack(target: Character): void {
    console.log(`${this.name} strikes ${target.name}!`);
  }
}

Part 7: Security with TypeScript

19. How Type Safety Improves Security

While not a silver bullet, a strong type system is a powerful security tool. It prevents entire classes of runtime errors that can lead to unpredictable behavior and vulnerabilities.

  • Null Pointer/Reference Errors: With "strictNullChecks": true, the compiler forces you to check for null or undefined before you can use a variable, preventing crashes and potential logic flaws.
  • Type Confusion Bugs: TypeScript ensures you can't accidentally use a number where a string is expected, or vice versa. In lower level languages, this type of bug can lead to memory corruption.
  • API Contract Enforcement: When working with typed objects, you get compile time guarantees that you are accessing properties that actually exist, preventing undefined is not a function errors.

20. The any Anti-Pattern

Using any is like telling the TypeScript compiler, "Trust me, I know what I'm doing." It completely disables type checking for that variable. This can hide bugs and vulnerabilities that TypeScript would otherwise catch.

function processData(data: any) {
  // The compiler has no idea what `data` is. It will allow anything.
  console.log(data.name.toUpperCase()); // This will crash if `data.name` doesn't exist!
}

Solution: Use unknown and perform type checks, or better yet, define an interface for the expected shape of data.

21. The Golden Rule: Runtime Validation is Still Required

This is the most important security lesson for any TypeScript developer. TypeScript types are a compile time tool. They are completely erased when your code is compiled to JavaScript.

This means TypeScript offers zero protection against malicious or malformed data coming from external sources at runtime.

interface User { name: string; }

// Imagine this function is in an Express.js API handler
function createUser(req: Request, res: Response) {
  // You might THINK `req.body` is a `User` because you typed it.
  const user: User = req.body;

  // But an attacker can send `req.body` as ` { "name": 123 } ` or ` { } `.
  // At runtime, `user.name` could be a number or undefined.
  // The `toUpperCase()` call will crash your server!
  console.log(user.name.toUpperCase()); 
}

22. Runtime Validation with zod

To solve the runtime validation problem, you need a library that can parse and validate untrusted data. Zod is a fantastic, popular choice.

First, install it: npm install zod

import { z } from "zod";

// 1. Define a schema that represents the shape and types you expect.
const UserSchema = z.object({
  username: z.string().min(3, "Username must be at least 3 characters"),
  email: z.string().email(),
  age: z.number().positive().optional(),
});

// 2. Infer the TypeScript type directly from the schema.
// Now your runtime schema and compile time type are always in sync!
type User = z.infer<typeof UserSchema>;

// 3. Parse untrusted data.
const untrustedData = { username: "test", email: "not an email" };

try {
  const validatedUser = UserSchema.parse(untrustedData);
  // If `parse` succeeds, you have a fully typed and validated object.
  console.log("Validation successful:", validatedUser);
} catch (error) {
  // If validation fails, Zod throws a detailed error.
  console.error("Validation failed:", error);
}

Part 8: Cookbook

23. Cookbook: A Complex User Profile Interface

This shows how to combine interfaces to define a complex data structure.

interface Address {
  street: string;
  city: string;
  zipCode: string;
  country: string;
}

enum UserRole {
  ADMIN = "ADMIN",
  EDITOR = "EDITOR",
  VIEWER = "VIEWER",
}

interface UserProfile {
  readonly id: string;
  username: string;
  email: string;
  address: Address;
  roles: UserRole[];
  lastLogin?: Date;
}

const user: UserProfile = {
  id: "uuid-12345",
  username: "0x1RIS",
  email: "0x1ris@example.com",
  address: {
    street: "123 Hacker Way",
    city: "Cyber City",
    zipCode: "1337",
    country: "NET",
  },
  roles: [UserRole.ADMIN, UserRole.EDITOR],
};

24. Cookbook: A Generic API Client

This generic function can fetch data from any API endpoint and will return a properly typed object, assuming the data is valid.

import { z, ZodType } from "zod";

// Use a runtime validator like Zod to ensure the API response is what you expect.
async function fetchAndValidate<T>(url: string, schema: ZodType<T>): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  const data = await response.json();

  // Parse the data against the schema. This is the runtime validation step!
  return schema.parse(data);
}

// Example usage:
const PostSchema = z.object({
  userId: z.number(),
  id: z.number(),
  title: z.string(),
  body: z.string(),
});

async function getPost() {
  try {
    const post = await fetchAndValidate("https://jsonplaceholder.typicode.com/posts/1", PostSchema);
    // `post` is now fully typed and validated!
    console.log("Post Title:", post.title);
  } catch (error) {
    console.error(error);
  }
}

getPost();

25. Cookbook: A Simple Class Hierarchy

This example shows basic OOP principles with classes, inheritance, and access modifiers.

abstract class Vehicle {
  constructor(protected readonly vin: string) {}

  drive(): void {
    console.log("The vehicle is moving.");
  }

  abstract honk(): void; // Must be implemented by subclasses
}

class Car extends Vehicle {
  private isEngineOn: boolean = false;

  constructor(vin: string, public color: string) {
    super(vin);
  }

  startEngine() {
    this.isEngineOn = true;
    console.log("Engine started.");
  }

  honk(): void {
    console.log("Beep beep!");
  }
}

const myCar = new Car("VIN12345", "blue");
myCar.startEngine();
myCar.drive();
myCar.honk();
// console.log(myCar.vin); // Error: 'vin' is protected and only accessible within class 'Vehicle' and its subclasses.