Unit 5 - Notes

INT219 6 min read

Unit 5: TypeScript Fundamentals

1. Introduction to TypeScript

What is TypeScript?

TypeScript is an open-source programming language developed and maintained by Microsoft. It is a strict syntactical superset of JavaScript, meaning any valid JavaScript code is also valid TypeScript code. TypeScript adds optional static typing to the language.

Key Characteristics

  • Static Typing: Types are checked at compile time, not runtime. This catches errors early in the development cycle.
  • Transpilation: Browsers and Node.js cannot execute TypeScript directly. It must be compiled (transpiled) into standard JavaScript.
  • ECMAScript Features: TypeScript supports the latest ECMAScript standards (ES6, ES2020, etc.) and compiles them down to older versions (like ES5) for browser compatibility.

Benefits over JavaScript

  • Early Error Detection: Catches typos, type mismatches, and logic errors before the code runs.
  • Enhanced IDE Support: Provides robust autocompletion (IntelliSense), code navigation (Go to Definition), and refactoring tools.
  • Scalability: Makes managing large codebases easier by making code self-documenting through types.
  • Maintainability: easier to onboard new developers as the data structures are explicitly defined.

2. Type System and Annotations

The core of TypeScript is the ability to annotate variables, parameters, and return values with specific types.

Basic Primitive Types

  • boolean: true or false.
  • number: Integers and floating-point values.
  • string: Textual data.

TYPESCRIPT
let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";

Special Types

  • any: Opt-out of type checking. Allows any value. (Use sparingly).
  • unknown: Safer version of any. You cannot perform operations on an unknown value without narrowing the type first.
  • void: Represents the absence of a value (commonly used as the return type for functions that return nothing).
  • null and undefined: Subtypes of all other types (unless strictNullChecks is enabled).
  • never: Represents values that never occur (e.g., functions that throw errors or have infinite loops).

Arrays and Tuples

  • Array: Lists of a single type.
  • Tuple: Fixed-length arrays where each element has a known, specific type.

TYPESCRIPT
// Arrays
let list: number[] = [1, 2, 3];
let genericList: Array<number> = [1, 2, 3];

// Tuple
let x: [string, number];
x = ["hello", 10]; // OK
// x = [10, "hello"]; // Error

Function Annotations

You can annotate function parameters and return types.

TYPESCRIPT
// (parameter: type): returnType
function add(x: number, y: number): number {
    return x + y;
}

// Optional parameters (?)
function greet(name: string, greeting?: string): void {
    console.log(`${greeting || 'Hello'}, ${name}`);
}


3. Interfaces and Type Aliases

Both Interfaces and Type Aliases allow you to define the shape of an object or custom types.

Interfaces

Interfaces are primarily used to define the structure of objects. They support extending other interfaces.

TYPESCRIPT
interface User {
    id: number;
    username: string;
    email?: string; // Optional property
    readonly role: string; // Cannot be changed after creation
}

const user1: User = {
    id: 1,
    username: "dev_one",
    role: "admin"
};

// Extending Interfaces
interface AdminUser extends User {
    permissions: string[];
}

Type Aliases

Type aliases can define objects, but they can also define primitives, unions, and tuples.

TYPESCRIPT
type Point = {
    x: number;
    y: number;
};

// Type alias for a primitive
type ID = string;

// Type alias for a function signature
type Callback = (data: string) => void;

Interface vs. Type Alias

Feature Interface Type Alias
Purpose Defining object shapes Defining shapes, unions, primitives
Extensibility Can be merged (declaration merging) Cannot be re-opened to add properties
Extension Syntax extends Intersection &
Best Practice Use for Objects/Classes APIs Use for Unions, Primitives, Complex Functions

4. Union and Intersection Types

These allow developers to combine existing types into new ones.

Union Types (|)

A variable with a union type can be one of several types. It represents an OR relationship.

TYPESCRIPT
let id: string | number;
id = 101;       // Valid
id = "202-A";   // Valid
// id = true;   // Error

// Literal Unions
type Direction = "North" | "South" | "East" | "West";
let move: Direction = "North";

Intersection Types (&)

An intersection type combines multiple types into one. The resulting object must have all properties of all intersected types. It represents an AND relationship.

TYPESCRIPT
interface Draggable {
    drag: () => void;
}

interface Resizable {
    resize: () => void;
}

type UIWidget = Draggable & Resizable;

let widget: UIWidget = {
    drag: () => {},
    resize: () => {}
};


5. Type Inference and Narrowing

TypeScript is smart enough to deduce types without explicit annotations, and it tracks how types change within control flow structures.

Type Inference

If you declare a variable and initialize it, TypeScript infers the type.

TYPESCRIPT
let message = "Hello World"; 
// TypeScript infers 'message' is type 'string'

// message = 100; // Error: Type 'number' is not assignable to type 'string'

Type Narrowing

When handling Union types, you must "narrow" the specific type before performing type-specific operations.

1. typeof Guard (Primitives)

TYPESCRIPT
function printId(id: number | string) {
    if (typeof id === "string") {
        console.log(id.toUpperCase()); // TS knows id is string here
    } else {
        console.log(id.toFixed(2));    // TS knows id is number here
    }
}

2. instanceof Guard (Classes)

TYPESCRIPT
class Dog { bark() {} }
class Cat { meow() {} }

function sound(animal: Dog | Cat) {
    if (animal instanceof Dog) {
        animal.bark();
    } else {
        animal.meow();
    }
}

3. Discriminated Unions
Using a common literal property (the "discriminant") to distinguish between interfaces.

TYPESCRIPT
interface Success {
    status: "success";
    data: string;
}

interface Failure {
    status: "error";
    error: string;
}

type Response = Success | Failure;

function handleResponse(res: Response) {
    if (res.status === "success") {
        console.log(res.data); // TS knows this is Success
    } else {
        console.log(res.error); // TS knows this is Failure
    }
}


6. Generics

Generics allow you to create reusable components that work with a variety of types rather than a single one, while still maintaining type safety. They are like variables for types.

Generic Functions

Using <T> (a conventional placeholder for "Type") to capture the type provided by the user.

TYPESCRIPT
// Without generics, we lose information (if using any)
// With generics:
function identity<T>(arg: T): T {
    return arg;
}

let output1 = identity<string>("myString"); // Type is string
let output2 = identity(100);                // Type inferred as number

Generic Interfaces

TYPESCRIPT
interface Box<T> {
    contents: T;
}

let stringBox: Box<string> = { contents: "Letters" };
let numberBox: Box<number> = { contents: 42 };

Generic Constraints

You can limit the types that can be passed to a generic by using extends.

TYPESCRIPT
interface Lengthwise {
    length: number;
}

// T must have a .length property
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); 
    return arg;
}

loggingIdentity({ length: 10, value: 3 }); // Valid
// loggingIdentity(3); // Error: Number doesn't have .length


7. TypeScript Compilation Workflow

Since TypeScript cannot run in the browser, understanding the compilation process is vital.

The Compiler (tsc)

The TypeScript Compiler converts .ts files into .js files.

  • Command: tsc filename.ts

tsconfig.json

This is the root configuration file for a TypeScript project. It specifies root files and compiler options.

  • Initialization: Run tsc --init to generate this file.

Key Configuration Options:

  • target: Specifies the ECMAScript target version (e.g., "es5", "es6", "es2020"). Older targets maximize compatibility; newer targets produce cleaner code.
  • module: Specifies the module code generation method (e.g., "commonjs" for Node, "esnext" for modern frontend tools).
  • outDir: The directory where compiled .js files will be placed (e.g., ./dist).
  • rootDir: The root directory of source .ts files (e.g., ./src).
  • strict: Enables all strict type-checking options (strongly recommended).
    • Includes noImplicitAny, strictNullChecks, etc.

Workflow Example

  1. Write Code: Developer writes src/index.ts.
  2. Compile: Developer runs tsc.
  3. Transpile: TypeScript validates types.
    • If errors: Compilation stops (or warns, depending on config), and errors are logged.
    • If success: dist/index.js is generated.
  4. Run: The generated JavaScript is executed by the browser or Node.js.

Integration with Bundlers

In modern web development (React, Vue, Angular), tsc is often integrated into bundlers like Webpack, Vite, or Parcel. These tools handle the compilation on-the-fly during development (Hot Module Replacement) and bundling for production.