1. javascript
  2. /typescript

Intro to TypeScript

TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale. Developed by Microsoft, TypeScript adds static type definitions to JavaScript, which helps catch errors during development rather than at runtime. This makes it particularly valuable for large-scale applications where code maintainability and reliability are crucial.

TypeScript is a superset of JavaScript, meaning that all valid JavaScript code is also valid TypeScript code. This makes it easy to migrate existing JavaScript projects to TypeScript incrementally. The TypeScript compiler transpiles TypeScript code into plain JavaScript, which can run anywhere JavaScript runs - in browsers, on servers, or in mobile applications.

One of the key advantages of TypeScript is its static typing system. While JavaScript is dynamically typed, TypeScript allows developers to specify types for variables, function parameters, return values, and object properties. This enables better IntelliSense, autocompletion, and refactoring support in code editors, significantly improving the development experience.

TypeScript also supports modern JavaScript features and often implements upcoming ECMAScript proposals before they're available in JavaScript engines. This means developers can use the latest language features while maintaining compatibility with older JavaScript environments through the compilation process.

Key Benefits and Features

TypeScript offers numerous advantages over plain JavaScript, making it an excellent choice for modern web development projects.

Static Type Checking is perhaps the most significant benefit. TypeScript's type system helps catch common programming errors at compile time, such as typos in property names, incorrect function arguments, or operations on null/undefined values. This leads to more robust code and fewer runtime errors.

Enhanced IDE Support is another major advantage. TypeScript provides rich IntelliSense, accurate autocompletion, and powerful refactoring capabilities. Modern editors like Visual Studio Code, WebStorm, and others offer excellent TypeScript support, making development more efficient and enjoyable.

Better Code Documentation comes naturally with TypeScript. Type annotations serve as inline documentation, making it easier for developers to understand what functions expect and return. This is particularly valuable in team environments and when working with large codebases.

Improved Refactoring is possible thanks to the type system. When you rename a function or change its signature, TypeScript can identify all the places in your codebase that need to be updated, making large-scale refactoring much safer and more reliable.

Modern JavaScript Features are available in TypeScript often before they're widely supported in browsers. Features like decorators, async/await, and optional chaining can be used in TypeScript and compiled down to compatible JavaScript for older environments.

Getting Started with TypeScript

To begin using TypeScript, you'll need to install it either globally or as a project dependency. The most common approach is to install it locally in your project using npm or yarn.

npm install -g typescript
# or locally
npm install --save-dev typescript

Once installed, you can create TypeScript files with the .ts extension for regular TypeScript files or .tsx for files containing JSX (React components). TypeScript files are compiled to JavaScript using the TypeScript compiler (tsc).

To compile a TypeScript file, you can use the command line compiler:

tsc filename.ts

For more complex projects, you'll typically create a tsconfig.json file that configures the TypeScript compiler for your entire project. This file specifies compiler options, file inclusions/exclusions, and other project-specific settings.

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "lib": ["es2018", "dom"],
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Modern development environments often integrate TypeScript compilation into the build process, so you might not need to run the compiler manually. Tools like webpack, Vite, or Create React App handle TypeScript compilation automatically.

Basic Types and Type Annotations

TypeScript's type system is one of its most powerful features. Understanding the basic types and how to use them is essential for effective TypeScript development.

Primitive Types include the basic JavaScript types with explicit type annotations:

let name: string = "John Doe";
let age: number = 30;
let isActive: boolean = true;
let value: null = null;
let data: undefined = undefined;

Arrays can be typed in two ways:

let numbers: number[] = [1, 2, 3, 4, 5];
let strings: Array<string> = ["hello", "world"];

Objects can have their structure defined using type annotations:

let person: { name: string; age: number } = {
  name: "Jane",
  age: 25
};

Functions can have typed parameters and return values:

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

const add = (a: number, b: number): number => {
  return a + b;
};

Union Types allow a value to be one of several types:

let id: string | number = "123";
id = 456; // Also valid

Type Aliases help create reusable type definitions:

type User = {
  id: number;
  name: string;
  email: string;
};

let user: User = {
  id: 1,
  name: "Alice",
  email: "[email protected]"
};

Interfaces and Object Types

Interfaces are one of TypeScript's most powerful features for defining the structure of objects. They provide a way to name and describe the shape of objects, making code more readable and maintainable.

Basic Interface Definition:

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

function createUser(userData: User): User {
  return {
    id: userData.id,
    name: userData.name,
    email: userData.email
  };
}

Optional Properties can be defined using the ? operator:

interface Product {
  id: number;
  name: string;
  description?: string; // Optional property
  price: number;
}

Readonly Properties prevent modification after object creation:

interface Point {
  readonly x: number;
  readonly y: number;
}

let origin: Point = { x: 0, y: 0 };
// origin.x = 1; // Error: Cannot assign to 'x' because it is a read-only property

Function Types can be defined in interfaces:

interface Calculator {
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
}

Index Signatures allow for dynamic property names:

interface StringDictionary {
  [key: string]: string;
}

let dictionary: StringDictionary = {
  hello: "world",
  foo: "bar"
};

Interface Inheritance allows extending existing interfaces:

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

interface Dog extends Animal {
  breed: string;
  bark(): void;
}

Classes and Object-Oriented Programming

TypeScript enhances JavaScript's class syntax with additional features like access modifiers, abstract classes, and strong typing for class members.

Basic Class Definition:

class Person {
  public name: string;
  private age: number;
  protected id: number;

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

  public introduce(): string {
    return `Hi, I'm ${this.name}`;
  }

  private getAge(): number {
    return this.age;
  }
}

Access Modifiers control the visibility of class members:

  • public: Accessible from anywhere (default)
  • private: Only accessible within the class
  • protected: Accessible within the class and its subclasses

Class Inheritance works similarly to other object-oriented languages:

class Employee extends Person {
  private jobTitle: string;

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

  public getJobInfo(): string {
    return `${this.name} works as a ${this.jobTitle}`;
  }
}

Abstract Classes provide base classes that cannot be instantiated directly:

abstract class Shape {
  abstract getArea(): number;
  
  public describe(): string {
    return `This shape has an area of ${this.getArea()}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  getArea(): number {
    return Math.PI * this.radius ** 2;
  }
}

Getter and Setter Methods provide controlled access to class properties:

class Temperature {
  private _celsius: number = 0;

  get celsius(): number {
    return this._celsius;
  }

  set celsius(value: number) {
    this._celsius = value;
  }

  get fahrenheit(): number {
    return (this._celsius * 9/5) + 32;
  }
}

Advanced TypeScript Features

TypeScript includes several advanced features that enable powerful type manipulation and code organization patterns.

Generics allow you to create reusable components that work with multiple types:

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

let stringResult = identity<string>("hello");
let numberResult = identity<number>(42);

// Generic interfaces
interface Container<T> {
  value: T;
  getValue(): T;
}

class Box<T> implements Container<T> {
  constructor(public value: T) {}
  
  getValue(): T {
    return this.value;
  }
}

Utility Types provide common type transformations:

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

// Partial makes all properties optional
type PartialUser = Partial<User>;

// Pick selects specific properties
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;

// Omit excludes specific properties
type UserWithoutPassword = Omit<User, 'password'>;

Conditional Types enable type logic based on conditions:

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

type Example1 = NonNullable<string | null>; // string
type Example2 = NonNullable<number | undefined>; // number

Mapped Types transform existing types:

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

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

Module Declaration allows you to declare types for external libraries:

declare module "my-library" {
  export function doSomething(value: string): number;
  export interface Config {
    apiKey: string;
    timeout: number;
  }
}

Working with Modern JavaScript Features

TypeScript supports and often anticipates modern JavaScript features, allowing developers to use cutting-edge syntax while maintaining broad compatibility.

Async/Await works seamlessly with TypeScript's type system:

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const userData: User = await response.json();
  return userData;
}

// Using the function
async function displayUser(id: number): Promise<void> {
  try {
    const user = await fetchUser(id);
    console.log(user.name);
  } catch (error) {
    console.error('Failed to fetch user:', error);
  }
}

Destructuring with type annotations:

interface ApiResponse {
  data: User[];
  total: number;
  page: number;
}

function processResponse({ data, total }: ApiResponse): void {
  console.log(`Processing ${data.length} of ${total} users`);
}

Optional Chaining prevents errors when accessing nested properties:

interface Company {
  name: string;
  address?: {
    street: string;
    city: string;
    country?: string;
  };
}

function getCountry(company: Company): string | undefined {
  return company.address?.country;
}

Nullish Coalescing provides fallback values:

function getDisplayName(user: User): string {
  return user.displayName ?? user.name ?? "Anonymous";
}

TypeScript in Different Environments

TypeScript can be used in various development environments and frameworks, each with its own setup and best practices.

React with TypeScript is a popular combination:

import React, { useState } from 'react';

interface Props {
  initialCount: number;
  onCountChange: (count: number) => void;
}

const Counter: React.FC<Props> = ({ initialCount, onCountChange }) => {
  const [count, setCount] = useState<number>(initialCount);

  const handleIncrement = (): void => {
    const newCount = count + 1;
    setCount(newCount);
    onCountChange(newCount);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
};

export default Counter;

Node.js with TypeScript enables server-side development:

import express, { Request, Response } from 'express';

const app = express();
const PORT = process.env.PORT || 3000;

interface CreateUserRequest {
  name: string;
  email: string;
}

app.use(express.json());

app.post('/users', (req: Request<{}, {}, CreateUserRequest>, res: Response) => {
  const { name, email } = req.body;
  
  // User creation logic here
  const newUser = { id: Date.now(), name, email };
  
  res.status(201).json(newUser);
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Testing with TypeScript using Jest:

interface Calculator {
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
}

class BasicCalculator implements Calculator {
  add(a: number, b: number): number {
    return a + b;
  }

  subtract(a: number, b: number): number {
    return a - b;
  }
}

// Test file
describe('BasicCalculator', () => {
  let calculator: Calculator;

  beforeEach(() => {
    calculator = new BasicCalculator();
  });

  test('should add two numbers correctly', () => {
    expect(calculator.add(2, 3)).toBe(5);
  });

  test('should subtract two numbers correctly', () => {
    expect(calculator.subtract(5, 3)).toBe(2);
  });
});

Best Practices and Common Patterns

Following TypeScript best practices ensures maintainable, scalable, and robust code.

Use Strict Mode: Enable strict TypeScript checking in your tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

Prefer Interfaces Over Type Aliases for object shapes, as interfaces are more extensible:

// Preferred
interface User {
  id: number;
  name: string;
}

// Less preferred for object shapes
type User = {
  id: number;
  name: string;
};

Use Discriminated Unions for type safety with different object variants:

interface LoadingState {
  status: 'loading';
}

interface SuccessState {
  status: 'success';
  data: any;
}

interface ErrorState {
  status: 'error';
  error: string;
}

type AsyncState = LoadingState | SuccessState | ErrorState;

function handleState(state: AsyncState): void {
  switch (state.status) {
    case 'loading':
      console.log('Loading...');
      break;
    case 'success':
      console.log('Data:', state.data);
      break;
    case 'error':
      console.error('Error:', state.error);
      break;
  }
}

Avoid any Type: Use more specific types or unknown when the type is truly unknown:

// Instead of any
function processData(data: any): void {
  // ...
}

// Use unknown and type guards
function processData(data: unknown): void {
  if (typeof data === 'string') {
    console.log(data.toUpperCase());
  } else if (typeof data === 'number') {
    console.log(data.toFixed(2));
  }
}

TypeScript has revolutionized JavaScript development by providing the benefits of static typing while maintaining the flexibility and expressiveness of JavaScript. Its growing ecosystem, excellent tooling support, and seamless integration with popular frameworks make it an excellent choice for modern web development projects of any scale.