Typescript Utility Types

Introduction

Hi there, I’m excited to talk to you today about Typescript Utility Types. First, let’s provide a brief overview of what Typescript is for those who may not be familiar with it. Typescript is an open-source programming language that builds on top of JavaScript by adding static types to code. This can help catch errors at compile-time rather than runtime, making for a more stable and reliable code base.

Within Typescript, we have access to what are known as Utility Types. These are pre-built type templates that can be used throughout a project to enforce consistency and accelerate development time. In this article, we’ll dive into what Utility Types are, what they can do, and how to best implement them into your code.

What are Utility Types?

Utility Types are pre-built generic types provided by Typescript that can be reused throughout a codebase. Essentially, they’re type templates that save developers time and help enforce consistency across projects. Rather than constantly writing out custom types, we can leverage Utility Types to quickly establish a foundation of typing within our applications.

Examples of Utility Types include Partial, Readonly, Record, Pick, Omit, Required, NonNullable, ReturnType, Parameters, InstanceType, ThisType, Exclude, and Extract. In this article, we’ll cover the most common built-in types and some external types that can be used to great effect.

Typescript Built-In Utility Types

Partial

The Partial type is exactly as it sounds – it makes all properties of an object optional. This is useful when you have a larger object with many properties, but you want to be able to define subsets of it without needing to recreate the entire object. Here’s an example:

interface User {
  firstName: string;
  lastName: string;
  email: string;
  password: string;
}

type PartialUser = Partial<User>; 

const initialUserState: PartialUser = {
  firstName: 'Jane'
} 

In the code above, we’ve created an interface for a User object and defined all of its properties as required. We then use the Partial type to create a new type called PartialUser, which makes all of the properties optional. Finally, we initialize an object of type PartialUser called initialUserState and provide only a value for the firstName property. The other properties can be added in later, if necessary.

Readonly

The Readonly type is useful for making an object’s properties immutable. This can add an extra layer of safety to code, as it prevents accidental mutations that could cause issues down the line. Here’s an example:

interface User {
  readonly firstName: string;
  lastName: string;
  email: string;
  password: string;
}

const user: User = {
  firstName: 'Jane',
  lastName: 'Doe',
  email: '[email protected]',
  password: 'password123'
}

user.firstName = 'John'; // Error: Cannot assign to 'firstName' because it is a read-only property.

In the above code, we’ve defined a User interface with a readonly firstName property. We then initialize a user object with all required properties. When we attempt to re-assign the firstName property to a new value, we get a compilation error due to the readonly nature of the property.

Record

The Record type is useful when you have a set of keys with corresponding values that you want to type. Here’s an example:

interface User {
  firstName: string;
  lastName: string;
  email: string;
}

type UserRecord = Record<string, User>;

const users: UserRecord = {
  '[email protected]': {
    firstName: 'Jane',
    lastName: 'Doe',
    email: '[email protected]
  },
  '[email protected]': {
    firstName: 'John',
    lastName: 'Smith',
    email: '[email protected]'
  }
}

In the above code, we create an interface for a User, then use the Record type to establish a UserRecord type that takes a string as a key and a User as a value. We then use this UserRecord type to declare an object called users that contains two User objects, both with unique string keys.

Pick

The Pick type is useful when you want to select a subset of properties from an object to create a new type. Here’s an example:

interface User {
  firstName: string;
  lastName: string;
  email: string;
  password: string;
}

type UserCredentials = Pick<User, 'email' | 'password'>;

const userCredentials: UserCredentials = {
  email: '[email protected]',
  password: 'password123'
}

In the code above, we use the Pick type to define a new type called UserCredentials, which only contains the email and password properties from the original User interface. We then create an object of type UserCredentials and only provide values for those two properties.

Omit

The Omit type does the opposite of the Pick type – it removes a subset of properties from an object to create a new type. Here’s an example:

interface User {
  firstName: string;
  lastName: string;
  email: string;
  password: string;
}

type UserPublic = Omit<User, 'password'>;

const publicUser: UserPublic = {
  firstName: 'Jane',
  lastName: 'Doe',
  email: '[email protected]'
}

In the code above, we define an User interface with four properties. We then use the Omit type to create a new type called UserPublic that excludes the password property. Finally, we create an object of type UserPublic called publicUser and provide values for all properties except password.

Required

The Required type is useful when you want to force all properties of an object to be required. Here’s an example:

interface User {
  firstName?: string;
  lastName?: string;
  email?: string;
  password?: string;
}

type RequiredUser = Required<User>;

const user: RequiredUser = {
  firstName: 'Jane',
  lastName: 'Doe',
  email: '[email protected]',
  password: 'password123'
}

In the code above, we define an User interface with all properties optional. We then use the Required type to create a new type called RequiredUser, which makes all properties required. Finally, we create an object of type RequiredUser called user and provide values for all properties.

NonNullable

The NonNullable type is useful when you want to exclude null and undefined from a type. Here’s an example:

interface User {
  firstName: string | null | undefined;
  lastName: string | null | undefined;
  email: string | null | undefined;
  password: string | null | undefined;
}

type NonNullableUser = NonNullable<User>;

const user: NonNullableUser = {
  firstName: 'Jane',
  lastName: 'Doe',
  email: '[email protected]',
  password: 'password123'
}

In the code above, we define an User interface with all properties potentially including null or undefined. We then use the NonNullable type to create a new type called NonNullableUser, which excludes null and undefined. Finally, we create an object of type NonNullableUser called user and provide values for all properties.

External Utility Types

ReturnType

The ReturnType type is an external utility type that can be used to extract the return type of a function. Here’s an example:

type AddFunction = (a: number, b: number) => number;

type AddFunctionReturn = ReturnType<AddFunction>;

const add: AddFunction = (a, b) => a + b;

const addResult: AddFunctionReturn = add(1, 2); // 3

In the code above, we first define an AddFunction type that takes two numbers and returns a number. We then use the ReturnType type to create a new type called AddFunctionReturn, which extracts the return type from the original AddFunction type. We then create a function called add that matches the AddFunction type and test it by passing in two numbers and verifying that the result is 3.

Parameters

The Parameters type is an external utility type that can be used to extract the parameter types from a function. Here’s an example:

type AddFunction = (a: number, b: number) => number;

type AddFunctionParams = Parameters<AddFunction>;

function add(a: number, b: number): AddFunctionParams {
  return [a, b];
}

const addParams: AddFunctionParams = add(1, 2); // [1, 2]

In the code above, we define an AddFunction type that takes two numbers and returns a number. We then use the Parameters type to create a new type called AddFunctionParams, which extracts the parameter types from the original AddFunction type. We then create a function called add that takes two numbers and returns the AddFunctionParams type (an array with two numbers). Finally, we test this function by calling it with two numbers and verifying that the result is an array with those two numbers.

InstanceType

The InstanceType type is an external utility type that can be used to extract the instance type of a class. Here’s an example:

class User {
  firstName: string;
  lastName: string;

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName;
    this.lastName = lastName;
  }
}

type UserInstance = InstanceType<typeof User>;

const user:UserInstance = new User('Jane', 'Doe');

console.log(user); // User { firstName: 'Jane', lastName: 'Doe' }

In the code above, we define a User class with a constructor that takes two string arguments (firstName and lastName). We then use the InstanceType type to create a new type called UserInstance, which extracts the instance type from the User class. Finally, we test this type by creating a new instance of the User class and verifying that it matches the expected output.

Exclude and Extract

The Exclude and Extract types are external utility types that can be used to filter types based on specific criteria. Here’s an example:

type Animal = 'cat' | 'dog' | 'bird';

type DogOrBird = Exclude<Animal, 'cat'>; // 'dog' | 'bird'
type OnlyCat = Extract<Animal, 'cat'>; // 'cat'

In the code above, we define an Animal type that contains three possible values (cat, dog, or bird). We then use the Exclude type to create a new type called DogOrBird, which only includes dog and bird values from the original Animal type (excluding cat). Finally, we use the Extract type to create a new type called OnlyCat, which only includes the cat value from the original Animal type (excluding dog and bird).

Conclusion

In conclusion, Typescript Utility Types can be extremely useful for both enforcing consistency and accelerating development time. By leveraging pre-built type templates, we can create strong typing foundations within our codebases and avoid writing custom types from scratch. In this article, we covered the most common built-in utility types as well as some external utility types that can be used to great effect.

Hopefully, this article has given you a better understanding of how to use Typescript Utility Types and how they can make your development experience smoother and more efficient. Happy coding!

Learn Typescript Generators
Typescript

Learn Typescript Generators

Introduction TypeScript is a typed superset of JavaScript that brings static type checking to JavaScript development. TypeScript adds some much-needed structure and safety to JavaScript programming by providing tools for catching errors at compile time instead of run time. A generator is a feature that TypeScript borrows from Python. Generators are a special kind of […]

Learn Typescript Mixins: Avoid Spaghetti Code
Typescript

Learn Typescript Mixins: Avoid Spaghetti Code

As a software developer, I’ve been using Typescript for most of my latest projects. One of the most interesting features I’ve come across is Typescript Mixins. These allow for simple composability of classes and functions, which can help to reduce code complexity and improve maintainability. In this article, I’m going to provide a comprehensive guide […]

Learn Typescript Modules: Organization & Reusability
Typescript

Learn Typescript Modules: Organization & Reusability

As a developer, I am always looking for ways to make my code more organized and efficient. One tool that has helped me achieve this goal is Typescript modules. In this article, I will be discussing Typescript modules in-depth, including how to create and import them, how to declare dependencies, and the benefits of using […]

Typescript Declaration Merging
Typescript

Typescript Declaration Merging

Introduction Typescript Declaration Merging is an essential feature of the Typescript language, which allows developers to extend existing code without modifying it. It can be a bit complex and overwhelming for beginners, but once you understand how it works, you’ll find it to be a powerful tool. As a developer, you will come across scenarios […]