DEV Community

Abhay Singh Rathore
Abhay Singh Rathore

Posted on

Advanced TypeScript Techniques for JavaScript Developers

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Type aliases provide a way to create more expressive types.

type UserID = string | number;

function getUser(id: UserID) {
  // implementation
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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' });
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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',
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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" }],
};
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)