Advanced TypeScript Techniques for JavaScript Developers

Taking Your TypeScript Skills to the Next Level

TypeScript, with its robust type system and seamless integration with JavaScript, has revolutionized the way developers approach large-scale applications. While many have embraced the basics of TypeScript, understanding its advanced features can significantly enhance your productivity and code quality. This comprehensive guide explores advanced TypeScript techniques that every seasoned JavaScript developer should master.

1. Advanced Types

Union and Intersection Types

Union types allow a variable to hold more than one type. This is particularly useful for functions that can return multiple types.

function formatDate(date: string | Date): string {
  if (date instanceof Date) {
    return date.toISOString();
  }
  return new Date(date).toISOString();
}

Intersection types, on the other hand, combine multiple types into one.

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

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

type ArtworksResponse = ArtworksData & ErrorHandling;

const handleResponse = (response: ArtworksResponse) => {
  if (response.success) {
    console.log(response.artworks);
  } else {
    console.log(response.error?.message);
  }
};

Literal Types and Type Aliases

Literal types restrict a variable to a specific value or a set of values.

type Direction = 'north' | 'east' | 'south' | 'west';

function move(direction: Direction) {
  console.log(`Moving ${direction}`);
}

move('north'); // Valid
// move('up'); // Error

Type aliases provide a way to create more expressive types.

type UserID = string | number;

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

2. Advanced Generics

Generics provide a way to create reusable components. By using generics, you can create components that work with any data type.

Generic Functions

Creating functions that can work with various data types can be achieved with generics.

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

let output1 = identity<string>("myString"); // Output type is 'string'
let output2 = identity<number>(42); // Output type is 'number'

Generic Constraints

Generics can be constrained to ensure they operate on a certain subset of types.

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

// loggingIdentity(3); // Error
loggingIdentity({ length: 10, value: 3 });

Using keyof and typeof

The keyof keyword creates a union type of the keys of an object type.

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

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

const person: Person = { name: 'John', age: 30 };
let name = getProperty(person, 'name');

3. Utility Types

TypeScript provides several utility types that help with common type transformations.

Partial

The Partial type makes all properties in a type optional.

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
  title: 'Learn TypeScript',
  description: 'Study the official documentation',
};

const todo2 = updateTodo(todo1, { description: 'Read TypeScript books' });

Pick and Omit

The Pick type constructs a type by picking a set of properties from another type.

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>;

const todo: TodoPreview = {
  title: 'Clean room',
  completed: false,
};

The Omit type constructs a type by omitting a set of properties from another type.

type TodoInfo = Omit<Todo, 'completed'>;

const todoInfo: TodoInfo = {
  title: 'Clean room',
  description: 'Clean the room thoroughly',
};

4. Advanced Decorators

Decorators are a powerful feature in TypeScript that allows you to modify classes and their members. They can be used to add metadata, change behavior, or inject dependencies.

Class Decorators

Class decorators are applied to the constructor of a class.

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

@sealed
class BugReport {
  type = "report";
  title: string;

  constructor(t: string) {
    this.title = t;
  }
}

Method Decorators

Method decorators are applied to the methods of a class.

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function(...args: any[]) {
    console.log(`Calling ${propertyKey} with arguments: ${args}`);
    return originalMethod.apply(this, args);
  };

  return descriptor;
}

class Calculator {
  @log
  add(a: number, b: number) {
    return a + b;
  }
}

const calculator = new Calculator();
calculator.add(2, 3); // Logs: "Calling add with arguments: 2,3"

Property Decorators

Property decorators are applied to properties within a class.

function readonly(target: any, propertyKey: string) {
  const descriptor: PropertyDescriptor = {
    writable: false,
  };
  return descriptor;
}

class Cat {
  @readonly
  name: string;

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

const cat = new Cat("Whiskers");
// cat.name = "Fluffy"; // Error: Cannot assign to 'name' because it is a read-only property.

5. Advanced Interface and Type Manipulation

Conditional Types

Conditional types allow you to create types that depend on a condition.

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"

Mapped Types

Mapped types allow you to create new types by transforming properties.

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

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

const point: Readonly<Point> = { x: 10, y: 20 };
// point.x = 5; // Error: Cannot assign to 'x' because it is a read-only property.

Recursive Types

Recursive types are types that reference themselves. They are useful for defining nested structures.

type JSONValue =
  | string
  | number
  | boolean
  | { [x: string]: JSONValue }
  | JSONValue[];

const jsonObject: JSONValue = {
  a: 1,
  b: "string",
  c: [true, { d: "nested" }],
};

6. Practical Examples and Use Cases

Advanced Form Handling with Generics

Creating flexible form handlers that can work with different types of forms.

interface Form<T> {
  values: T;
  errors: Partial<Record<keyof T, string>>;
}

function handleSubmit<T>(form: Form<T>) {
  console.log(form.values);
}

interface LoginForm {
  username: string;
  password: string;
}

const loginForm: Form<LoginForm> = {
  values: { username: "user1", password: "pass" },
  errors: {},
};

handleSubmit(loginForm);

Type-safe API Requests

Ensuring API requests and responses are type-safe using generics and utility types.

interface ApiResponse<T> {
  data: T;
  error?: string;
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  const data = await response.json();
  return { data };
}

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

async function getUser(id: number) {
  const response = await fetchData<User>(`/api/users/${id}`);
  if (response.error) {
    console.error(response.error);
  } else {
    console.log(response.data);
  }
}

7. Conclusion

Mastering advanced TypeScript techniques allows you to write more robust, maintainable, and scalable code. By leveraging union and intersection types, generics, utility types, decorators, and advanced interface manipulations, you can enhance your development workflow and tackle complex applications with confidence.

Embrace these advanced TypeScript features and continue to push the boundaries of what you can achieve with this powerful language.

Happy coding

Did you find this article valuable?

Support Blogs by becoming a sponsor. Any amount is appreciated!