The Ultimate TypeScript Guide: From Novice to Expert
TypeScript
JavaScript
Programming
Web Development

The Ultimate TypeScript Guide: From Novice to Expert

12 min read

The Ultimate TypeScript Guide: From Novice to Expert

Table of Contents

  1. Introduction
  2. Basic Types
  3. Advanced Types
  4. Functions
  5. Classes and Interfaces
  6. Generics
  7. Type Manipulation
  8. Modules and Namespaces
  9. Decorators
  10. Advanced Patterns
  11. TypeScript Compiler and Configuration
  12. TypeScript with React
  13. Testing in TypeScript
  14. Performance Optimization
  15. Real-World Project: Building a Type-Safe API Client

Introduction

TypeScript is a powerful superset of JavaScript that adds static typing and other features to enhance developer productivity and code quality. This guide will take you from the basics to advanced concepts, providing a comprehensive understanding of TypeScript.

Basic Types

TypeScript provides several basic types to help you describe the shape of your data:

// Boolean
let isDone: boolean = false;

// Number
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

// String
let color: string = "blue";
color = "red";

// Array
let list: number[] = [1, 2, 3];
let fruits: Array<string> = ["apple", "banana", "orange"];

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

// Enum
enum Color {
  Red,
  Green,
  Blue,
}
let c: Color = Color.Green;

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

// Void
function warnUser(): void {
  console.log("This is my warning message");
}

// Null and Undefined
let u: undefined = undefined;
let n: null = null;

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

// Object
let obj: object = { key: "value" };

Advanced Types

TypeScript offers advanced type features for more complex scenarios:

Union Types

function padLeft(value: string, padding: string | number) {
  // ...
}

let indentedString = padLeft("Hello world", 4); // Works

Intersection Types

interface ErrorHandling {
  success: boolean;
  error?: { message: string };
}

interface ArtworksData {
  artworks: { title: string }[];
}

type ArtworksResponse = ErrorHandling & ArtworksData;

const response: ArtworksResponse = {
  success: true,
  artworks: [{ title: "Mona Lisa" }],
};

Type Aliases

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;

function getName(n: NameOrResolver): Name {
  if (typeof n === "string") {
    return n;
  } else {
    return n();
  }
}

Literal Types

type Easing = "ease-in" | "ease-out" | "ease-in-out";

function animate(dx: number, dy: number, easing: Easing) {
  // ...
}

animate(0, 0, "ease-in");
// animate(0, 0, "linear"); // Error: Argument of type '"linear"' is not assignable to parameter of type 'Easing'.

Index Types

function pluck<T, K extends keyof T>(o: T, propertyNames: K[]): T[K][] {
  return propertyNames.map((n) => o[n]);
}

interface Car {
  manufacturer: string;
  model: string;
  year: number;
}

let taxi: Car = {
  manufacturer: "Toyota",
  model: "Camry",
  year: 2014,
};

let makeAndModel: string[] = pluck(taxi, ["manufacturer", "model"]);

Functions

TypeScript enhances JavaScript functions with type annotations and more:

Function Types

function add(x: number, y: number): number {
  return x + y;
}

let myAdd: (x: number, y: number) => number = function (
  x: number,
  y: number
): number {
  return x + y;
};

Optional and Default Parameters

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

function buildName2(firstName: string, lastName = "Smith") {
  return firstName + " " + lastName;
}

Rest Parameters

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

Function Overloads

function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
function padding(a: number, b?: number, c?: number, d?: number) {
  if (b === undefined && c === undefined && d === undefined) {
    b = c = d = a;
  } else if (c === undefined && d === undefined) {
    c = a;
    d = b;
  }
  return {
    top: a,
    right: b,
    bottom: c,
    left: d,
  };
}

Classes and Interfaces

TypeScript provides full support for classes and interfaces:

Classes

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

class Snake extends Animal {
  constructor(name: string) {
    super(name);
  }
  move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters);
  }
}

Interfaces

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) {}
}

Generics

Generics allow you to create reusable components:

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

let output = identity<string>("myString");

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;
};

Generic Constraints

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;
}

Type Manipulation

TypeScript provides powerful ways to manipulate and transform types:

Mapped Types

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type Partial<T> = {
  [P in keyof T]?: T[P];
};

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

type ReadonlyPerson = Readonly<Person>;

Conditional Types

type NonNullable<T> = T extends null | undefined ? never : T;

type ExtractType<T, U> = T extends U ? T : never;

type StringOrNumber = ExtractType<string | number | boolean, string | number>;

Infer Keyword

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

function f() {
  return { x: 10, y: 3 };
}
type FReturnType = ReturnType<typeof f>; // { x: number, y: number }

Modules and Namespaces

TypeScript supports both ES modules and its own namespace system:

ES Modules

// math.ts
export function add(x: number, y: number): number {
  return x + y;
}

// main.ts
import { add } from "./math";
console.log(add(1, 2));

Namespaces

namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }

  const lettersRegexp = /^[A-Za-z]+$/;
  const numberRegexp = /^[0-9]+$/;

  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
      return lettersRegexp.test(s);
    }
  }

  export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
      return s.length === 5 && numberRegexp.test(s);
    }
  }
}

let validators: { [s: string]: Validation.StringValidator } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

Decorators

Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members:

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

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

Advanced Patterns

Mixins

// Disposable Mixin
class Disposable {
  isDisposed: boolean;
  dispose() {
    this.isDisposed = true;
  }
}

// Activatable Mixin
class Activatable {
  isActive: boolean;
  activate() {
    this.isActive = true;
  }
  deactivate() {
    this.isActive = false;
  }
}

class SmartObject implements Disposable, Activatable {
  constructor() {
    setInterval(
      () => console.log(this.isActive + " : " + this.isDisposed),
      500
    );
  }

  interact() {
    this.activate();
  }

  // Disposable
  isDisposed: boolean = false;
  dispose: () => void;
  // Activatable
  isActive: boolean = false;
  activate: () => void;
  deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);

let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);

////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////

function applyMixins(derivedCtor: any, baseCtors: any[]) {
  baseCtors.forEach((baseCtor) => {
    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
      Object.defineProperty(
        derivedCtor.prototype,
        name,
        Object.getOwnPropertyDescriptor(baseCtor.prototype, name)
      );
    });
  });
}

Builder Pattern with Method Chaining

class RequestBuilder {
  private data: object | null = null;
  private method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET";
  private url: string = "";

  setMethod(method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH"): this {
    this.method = method;
    return this;
  }

  setData(data: object): this {
    this.data = data;
    return this;
  }

  setURL(url: string): this {
    this.url = url;
    return this;
  }

  send(): Promise<Response> {
    return fetch(this.url, {
      method: this.method,
      body: this.data ? JSON.stringify(this.data) : null,
      headers: {
        "Content-Type": "application/json",
      },
    });
  }
}

// Usage
new RequestBuilder()
  .setMethod("POST")
  .setURL("https://api.example.com/data")
  .setData({ name: "John Doe" })
  .send()
  .then((response) => response.json())
  .then((data) => console.log(data));

TypeScript Compiler and Configuration

Understanding the TypeScript compiler (tsc) and its configuration options is crucial:

tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": ["dom", "es2015"],
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

Compiler API

import * as ts from "typescript";

function compile(fileNames: string[], options: ts.CompilerOptions): void {
  let program = ts.createProgram(fileNames, options);
  let emitResult = program.emit();

  let allDiagnostics = ts
    .getPreEmitDiagnostics(program)
    .concat(emitResult.diagnostics);

  allDiagnostics.forEach((diagnostic) => {
    if (diagnostic.file) {
      let { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
        diagnostic.start!
      );
      let message = ts.flattenDiagnosticMessageText(
        diagnostic.messageText,
        "\n"
      );
      console.log(
        `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`
      );
    } else {
      console.log(
        ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")
      );
    }
  });

  let exitCode = emitResult.emitSkipped ? 1 : 0;
  console.log(`Process exiting with code '${exitCode}'.`);
  process.exit(exitCode);
}

compile(process.argv.slice(2), {
  noEmitOnError: true,
  noImplicitAny: true,
  target: ts.ScriptTarget.ES5,
  module: ts.ModuleKind.CommonJS,
});

TypeScript with React

TypeScript integrates seamlessly with React, providing type safety for props, state, and more:

import React, { useState, useEffect } from "react";

interface User {
  id: number;
  name: string;
  email: string;
}

interface UserListProps {
  title: string;
}

const UserList: React.FC<UserListProps> = ({ title }) => {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState<boolean>(true);

  useEffect(() => {
    fetchUsers();
  }, []);

  const fetchUsers = async () => {
    try {
      const response = await fetch("https://api.example.com/users");
      const data: User[] = await response.json();
      setUsers(data);
      setLoading(false);
    } catch (error) {
      console.error("Error fetching users:", error);
      setLoading(false);
    }
  };

  return (
    <div>
      <h1>{title}</h1>
      {loading ? (
        <p>Loading...</p>
      ) : (
        <ul>
          {users.map((user) => (
            <li key={user.id}>
              {user.name} ({user.email})
            </li>
          ))}
        </ul>
      )}
    </div>
  );
};

export default UserList;

Testing in TypeScript

Testing TypeScript code involves using testing frameworks like Jest along with TypeScript-specific tools:

// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

// math.test.ts
import { add } from "./math";

describe("add function", () => {
  it("should add two numbers correctly", () => {
    expect(add(1, 2)).toBe(3);
    expect(add(-1, 1)).toBe(0);
    expect(add(5, 5)).toBe(10);
  });

  it("should return NaN for invalid inputs", () => {
    expect(add(1, NaN)).toBeNaN();
    expect(add(NaN, 1)).toBeNaN();
  });
});

Performance Optimization

TypeScript can help with performance optimization through proper typing and compiler optimizations:

// Using const assertions for better performance
const config = {
  endpoint: "https://api.example.com",
  apiKey: "your-api-key",
} as const;

// Type-based optimizations
type Vec2D = [number, number];

function addVectors(v1: Vec2D, v2: Vec2D): Vec2D {
  return [v1[0] + v2[0], v1[1] + v2[1]];
}

// Performance-critical loop
function sumArray(arr: number[]): number {
  let sum = 0;
  for (let i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
  return sum;
}

// Using generics for flexible, type-safe functions
function quickSort<T>(arr: T[], compare: (a: T, b: T) => number): T[] {
  if (arr.length <= 1) return arr;

  const pivot = arr[Math.floor(arr.length / 2)];
  const left = arr.filter((x) => compare(x, pivot) < 0);
  const middle = arr.filter((x) => compare(x, pivot) === 0);
  const right = arr.filter((x) => compare(x, pivot) > 0);

  return [...quickSort(left, compare), ...middle, ...quickSort(right, compare)];
}

Real-World Project: Building a Type-Safe API Client

Let's build a type-safe API client using TypeScript, demonstrating many of the concepts we've covered:

// api-types.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

export interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

export interface Comment {
  id: number;
  postId: number;
  name: string;
  email: string;
  body: string;
}

// api-client.ts
import axios, { AxiosInstance, AxiosResponse } from "axios";
import { User, Post, Comment } from "./api-types";

class APIClient {
  private client: AxiosInstance;

  constructor(baseURL: string) {
    this.client = axios.create({
      baseURL,
      headers: {
        "Content-Type": "application/json",
      },
    });
  }

  private async get<T>(url: string): Promise<T> {
    const response: AxiosResponse<T> = await this.client.get(url);
    return response.data;
  }

  private async post<T>(url: string, data: any): Promise<T> {
    const response: AxiosResponse<T> = await this.client.post(url, data);
    return response.data;
  }

  async getUsers(): Promise<User[]> {
    return this.get<User[]>("/users");
  }

  async getUser(id: number): Promise<User> {
    return this.get<User>(`/users/${id}`);
  }

  async getPosts(): Promise<Post[]> {
    return this.get<Post[]>("/posts");
  }

  async getPost(id: number): Promise<Post> {
    return this.get<Post>(`/posts/${id}`);
  }

  async getComments(postId: number): Promise<Comment[]> {
    return this.get<Comment[]>(`/posts/${postId}/comments`);
  }

  async createPost(post: Omit<Post, "id">): Promise<Post> {
    return this.post<Post>("/posts", post);
  }
}

// Usage
const apiClient = new APIClient("https://jsonplaceholder.typicode.com");

async function fetchAndDisplayUserPosts(userId: number) {
  try {
    const user = await apiClient.getUser(userId);
    console.log(`Fetching posts for user: ${user.name}`);

    const posts = await apiClient.getPosts();
    const userPosts = posts.filter((post) => post.userId === userId);

    for (const post of userPosts) {
      console.log(`Post: ${post.title}`);
      const comments = await apiClient.getComments(post.id);
      console.log(`Comments: ${comments.length}`);
    }
  } catch (error) {
    console.error("Error fetching data:", error);
  }
}

fetchAndDisplayUserPosts(1);

This project demonstrates:

  • Type definitions for API responses
  • A generic API client with type-safe methods
  • Error handling
  • Async/await usage
  • Filtering and mapping of typed arrays

Conclusion

This comprehensive guide covers the breadth and depth of TypeScript, from basic concepts to advanced patterns and real-world applications. By mastering these concepts, you'll be well-equipped to build robust, scalable, and maintainable applications using TypeScript.

Additional Resources