· engineering · 9 min read

Advanced TypeScript features you may not know about

Improve your TypeScript code with these must-have features

Improve your TypeScript code with these must-have features

TypeScript has become increasingly popular as a tool for web development due to its ability to enhance JavaScript with static type checking and other advanced features. These features can help developers write more maintainable and error-free code, leading to better application reliability.

While many developers are already familiar with the basics of TypeScript, there are a variety of advanced features that can provide even more benefits. These include things like generics, template literal types, type guards and infer keyword, which can be used to create more precise and flexible code.

This article will delve into these advanced TypeScript features and explain how they can be utilized to solve common problems and improve web application robustness. Whether you’re a beginner or a seasoned developer looking to expand your knowledge, this guide will provide valuable insights into the powerful capabilities of TypeScript.

Jump ahead:

  1. Generics
  2. Template Literal Types
  3. Type Guards
  4. Infer Keyword
  5. Conclusion

1. Generics

Generics offer a means of creating code that is adaptable and capable of working with multiple data types, rather than being restricted to a single type. This enables users of the code to specify their own types, providing greater flexibility and reusability.

To define a generic type in TypeScript, you can use angle brackets (<>) and a placeholder name for the type, which can then be used as a type annotation in function or class declarations. For example, you might define a function that takes an array of a certain type and returns a new array with that type:

function reverse<T>(arr: T[]): T[] {
  return arr.reverse();
}

In this example, the <T> syntax defines a generic type placeholder that can be used to represent any type of array. The function then takes an array of this generic type and returns a new array with the same type.

This can be useful for creating more reusable and type-safe code, as the function can be called with different types of arrays without having to redefine the function for each type.

Generics can also be used with classes to create reusable and type-safe data structures. For example, you might define a generic class for a queue data structure:

class Queue<T> {
  private items: T[] = [];

  enqueue(item: T) {
    this.items.push(item);
  }

  dequeue(): T | undefined {
    return this.items.shift();
  }
}

In this example, the <T> syntax is used to define a generic type placeholder for the items in the queue. This allows the class to be used with different types of items, while still providing type safety and avoiding the need for duplicated code.

Overall, generics are a powerful feature of TypeScript that can greatly improve the flexibility and maintainability of your code. By defining generic types that can be used across different parts of your code, you can create more reusable and type-safe functions and classes that are easier to maintain and extend.

2. Template Literal Types

Template Literal Types are a powerful feature in TypeScript that allow you to create complex types by combining literal strings and expressions in a template-like format. Introduced in version 4.1, they provide a flexible way to define types that are more precise and reusable.

To define a Template Literal Type, you use backticks (`) to enclose a string template that can include placeholders for expressions.

type NumberAndString = `${number}-${string}`;

Let’s say you have a set of heading and paragraph tags, and you want to create a type that represents all possible combinations of these tags with a “tag” suffix. You could define the heading and paragraph tags as string literals:

type Headings = "h1" | "h2" | "h3" | "h4" | "h5";
type Paragraphs = "p";

Then, you can use a Template Literal Type to concatenate these literals with the “tag” suffix:

type AllLocaleIDs = `${Headings | Paragraphs}_tag`;

In this example, the AllLocaleIDs type is defined using a string template that combines the Headings and Paragraphs literals with the “_tag” suffix to create all possible tag combinations such as "h1_tag", "h2_tag", "p_tag", and so on.

You can use this type to create reusable and precise types for various scenarios, such as in defining CSS class names:

function addClass(className: AllLocaleIDs) {
  // ...
}

In this example, the AllLocaleIDs type is used to ensure that only valid tag names with the “tag” suffix are accepted as the className argument.

Template Literal Types provide a flexible way to define more precise and reusable types in TypeScript, especially when used in combination with other type features like union and intersection types.

3. Type Guards

Type Guards are a TypeScript feature that allow you to check the type of a variable at runtime and perform different actions depending on the type. In other words, they provide a way to narrow down the type of a variable within a conditional statement. This is useful when working with variables that could have more than one possible type.

TypeScript provides some built-in JavaScript operators that can be used as type guards, including the typeof, instanceof, and in operators. Type guards can be used to detect the correct methods, prototypes, and properties of a value, similar to feature detection. They help ensure that the type of an argument is what you say it is, by allowing you to instruct the TypeScript compiler to infer a specific type for a variable in a particular context.

There are five major ways to use a type guard:

  • The instanceof keyword
  • The typeof keyword
  • The in keyword
  • Equality narrowing type guard
  • Custom type guard with predicate

In this article, we will cover the first three methods.

The instanceof type guard checks if a value is an instance of a given constructor function or class. With this type guard, we can test if an object or value is derived from a class, which is useful for determining the type of an instance. The syntax for the instanceof type guard is:

objectVariable instanceof ClassName;

In the following example, we use the instanceof type guard to determine the type of an Accessory object:

interface Accessory {
  brand: string;
}

class Necklace implements Accessory {
  kind: string;
  brand: string;

  constructor(brand: string, kind: string) {    
    this.brand = brand;
    this.kind = kind;
  }
}

class Bracelet implements Accessory {
  brand: string;
  year: number;

  constructor(brand: string, year: number) {    
    this.brand = brand;
    this.year = year;
  }
}

const getRandomAccessory = () => {
  return Math.random() < 0.5 ?
    new Bracelet('Cartier', 2021) :
    new Necklace('Choker', 'TASAKI');
}

let accessory = getRandomAccessory();

if (accessory instanceof Bracelet) {
  console.log(accessory.year);
}

if (accessory instanceof Necklace) {
  console.log(accessory.brand);    
}

The typeof type guard is used to determine the type of a variable. The typeof operator is said to be limited and shallow because it can only determine certain types recognized by JavaScript, such as boolean, string, bigint, symbol, undefined, function, and number. For anything outside of this list, the typeof type guard simply returns object. The syntax for the typeof type guard is:

typeof v !== "typename"
// or 
typeof v === "typename"

In the following example, we use the typeof type guard to determine the type of a variable x:

function StudentId(x: string | number) {
  if (typeof x == 'string') {
    console.log('Student');
  }

  if (typeof x === 'number') {
    console.log('ID');
  }
}

StudentId(`446`); // prints 'Student'
StudentId(446); // prints 'ID'

The in type guard checks if an object has a particular property, using that to differentiate between different types. It returns a boolean that indicates if the property exists in that object. The syntax for the in type guard is:

propertyName in objectName

Another similar example of how the in type guard works is shown below:

interface Cat {
  name: string;
  purr(): void;
}

interface Dog {
  name: string;
  bark(): void;
}

function isCatOrDog(pet: Cat | Dog): pet is Cat | Dog {
  return 'purr' in pet;
}

function petSounds(pet: Cat | Dog) {
  if (isCatOrDog(pet)) {
    if ('purr' in pet) {
      pet.purr();
    } else {
      pet.bark();
    }
  }
}

TypeScript type guards are helpful for assuring the value of a type, improving the overall code flow. Most of the time, your use case can be solved using either the instanceof type guard, the typeof type guard, or the in type guard, however, you can use a custom type guard when it is absolutely necessary.

4. Infer Keyword

infer is a keyword in TypeScript used in conditional types to infer a type from another type. It allows you to extract and use a type from a given type, which can be useful for building generic types that work with different data structures.

For example, we can declare a new type variable “R” in a type “MyType” that gets inferred from “T”:

type MyType<T> = T extends infer R ? R : never;
type T1 = MyType<{b: string}> // T1 is { b: string; }

Here, T1 is inferred to be ”{ b: string; }” because the type ”{ b: string; }” is assignable to R.

If we try to use an undeclared type parameter without using “infer”, the compiler will throw a compile error:

type MyType2<T> = T extends R2 ? R2 : never; // error, R2 undeclared

On the other hand, if we omit infer and compare T directly to R the compiler checks if T is assignable to R:

type R = { a: number }
type MyType3<T> = T extends R ? R : never;
type T3 = MyType3<{b: string}> // T3 is never

In this case, T3 is never because ”{ b: string; }” is not assignable to ”{ a: number; }“.

Finally, note that infer R shadows type references of an equally-named type declaration R:

type R = { a: number }
type MyType4<T> = T extends infer R ? R : never;
type T4 = MyType4<{b: string}> // T4 is { b: string; }

Here, T4 is inferred to be ”{ b: string; }” because the type variable R declared in MyType4 shadows the type reference of the equally-named type declaration R.

Conclusion

In conclusion, TypeScript is a powerful and flexible language that offers many advanced features beyond basic type annotations. By leveraging these features, developers can write safer, more maintainable code with fewer errors and better readability. Some of the features discussed in this article include generics, template literal types, type guards and the infer keyword.

By mastering these features and incorporating them into your development workflow, you can take full advantage of TypeScript’s capabilities and write more efficient, robust code.

I hope you enjoyed this article!

Seerat Awan

Tech Enthusiast | Frontend Developer

@seeratawan01

Subscribe to my newsletter to get the latest updates on my blog.

Back to Blog