TypeScript's built-in utilities: the key to writing maintainable test code

TypeScript's built-in utilities: the key to writing maintainable test code

Maximising test efficiency with TypeScript's built-in utilities

A set of built-in type utilities in TypeScript lets you manipulate and operate with types differently.

Common Type Utilities:

Common type utilities include:

  • Partial<T>: Makes all properties of a type T optional.

  • Omit<T, K>: Creates a new type that omits a specific set of properties K from the type T.

  • Exclude<T, U>: Creates a new type that represents the set of values from T that are not assignable to U.

  • NonNullable<T>: Creates a new type that represents the non-nullable version of T.

  • Record<K, T>: Creates a new type that represents a record of values with keys of type K and values of type T.

  • Extract<T, U>: Creates a new type that represents the intersection of T and U, i.e., the set of properties that are present in both T and U.

  • Pick<T, K>: Creates a new type that consists of a subset of the properties of T, specified by the keys in the union type K.

  • Readonly<T>: Makes all properties of a type T read-only.

  • Awaited<T>: Utility is used to mark a type as "unwrapped" when used in an await expression.

These type utilities enable you to build adaptable and type-safe test suite components in various test automation settings.

Utilities & its use cases:

Here are some instances where the built-in type utilities could be used in API testing scenarios:

Partial<T>

This can be used to construct a type for a request body object with optional properties. When the API permits you to omit particular information from the request body, this can be handy.

For Example:

interface CreateUserRequest {
  username: string;
  password: string;
  email?: string;
}

// Make all properties of CreateUserRequest optional
type PartialCreateUserRequest = Partial<CreateUserRequest>;

function createUser(request: PartialCreateUserRequest): Promise<void> {
  // Send request to API to create a new user
  return api.post('/users', request);
}

const request: PartialCreateUserRequest = {
  username: 'Srinivasan Sekar',
  password: 'mypass',
};

createUser(request);
// Sends request to API with only the 'username' and 'password' field

Omit<T, K>

You can use Omit<T, K> to define a type for a response object that excludes specific fields. This is useful if you wish to omit specific fields from the return data that are irrelevant to your testing.

For Example:

interface GetUserResponse {
  id: number;
  username: string;
  address: string;
  email: string;
}

// Omit the 'address' field from GetUserResponse
type OmitGetUserResponse = Omit<GetUserResponse, 'address'>;

function getUser(id: number): Promise<OmitGetUserResponse> {
  // Send request to API to get user data
  return api.get(`/users/${id}`);
}

getUser(1).then((response: OmitGetUserResponse) => {
  console.log(response);
  // { id: 1, username: 'srini', email: 'srini@example.com' }
});

Exclude<T, U>

You can use Exclude<T, U> to create a type for a request parameter that is restricted to a specific set of values.

For Example:

type OrderStatus = 'pending' | 'completed' | 'cancelled';

// Create a type for order status values that excludes 'cancelled'
type NonCancelledOrderStatus = Exclude<OrderStatus, 'cancelled'>;

function updateOrderStatus(orderId: number, status: NonCancelledOrderStatus): Promise<void> {
  // Send request to API to update order status
  return api.post(`/orders/${orderId}`, { status });
}

updateOrderStatus(1, 'pending');
// Sends request to API to set order status to 'pending'

updateOrderStatus(1, 'cancelled');
// This call would cause a type error because 'cancelled' is excluded from the allowed values for the 'status' parameter

In this case, the NonCancelledOrderStatus type provides a set of order status values that do not include the cancelled value. When you want to verify that the API does not accept requests to set the order status to cancelled, this can be handy.

NonNullable<T>

This can be used to build a type for a response object that guarantees that specific fields are not null. When you want to verify that specific fields are always present in the return data, this can be handy.

interface GetUserResponse {
  id: number | null;
  username: string | null;
  email: string | null;
}

// Create a type for the GetUserResponse that guarantees that the 'id' and 'username' fields are non-null
type NonNullableGetUserResponse = NonNullable<Pick<GetUserResponse, 'id' | 'username'>>;

function getUser(id: number): Promise<NonNullableGetUserResponse> {
  // Send request to API to get user data
  return api.get(`/users/${id}`);
}

getUser(10).then((response: NonNullableGetUserResponse) => {
  console.log(response);
  //  { id: 1, username: 'srini' }
});

In this example, the NonNullableGetUserResponse type represents a version of the GetUserResponse type where the id and username fields are guaranteed to be non-null. This can be useful when you want to ensure that these fields are always present in the response data and avoid null reference errors in your tests.

Record<K, T>

This can be used to construct a type for a map of objects with a given set of properties. This can be handy when storing and retrieving data in a type-safe manner.

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

// Create a type for a map that maps user IDs to User objects
type UserMap = Record<number, User>;

const users: UserMap = {
  1: {
    id: 1,
    username: 'srini',
    email: 'srini@example.com',
  },
  2: {
    id: 2,
    username: 'sekar',
    email: 'sekar@example.com',
  },
};

function getUserById(id: number): User {
  return users[id];
}

const user = getUserById(2);

Extract<T, U>

This can be used to build a type that represents the intersection of two object types. When you want to establish a type that represents a shared collection of properties between two objects.

// Define the request and response types for the API
interface CreateUserRequest {
  name: string;
  email: string;
  password: string;
}

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

// Create a type for a function that sends a create user request and returns a create user response
type CreateUserClient = (request: CreateUserRequest) => Promise<CreateUserResponse>;

// Extract the common properties of CreateUserRequest and CreateUserResponse
type CreateUserClientRequestResponse = Extract<CreateUserRequest, CreateUserResponse>;

function logCreateUserRequestResponse(request: CreateUserClientRequestResponse): void {
  console.log(`Name: ${request.name}`);
  console.log(`Email: ${request.email}`);
}

const apiClient: CreateUserClient = (request) => {
  // Send request to API and return response
  return api.createUser(request);
};

apiClient({
  name: 'John Doe',
  email: 'john.doe@example.com',
  password: 'password123',
}).then((response: CreateUserResponse) => {
  logCreateUserRequestResponse({ ...request, ...response });
});

The CreateUserClientRequestResponse type represents the intersection of the CreateUserRequest and CreateUserResponse types, which include only the name and email properties that are present in both types. The logCreateUserRequestResponse() the function is modified to only log the name and email properties of the CreateUserClientRequestResponse object.

Pick<T, K>

This utility in TypeScript creates a type that has a set of properties that are present in both T and K.

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

type UserPublicInfo = Pick<User, 'id' | 'name' | 'email'>;

function logUserPublicInfo(user: UserPublicInfo): void {
  console.log(`ID: ${user.id}`);
  console.log(`Name: ${user.name}`);
  console.log(`Email: ${user.email}`);
}

const user: User = {
  id: 123,
  name: 'Srini Sekar',
  email: 'Srini@example.com',
  password: 'password@123',
};

logUserPublicInfo(user);

In this example, the UserPublicInfo type represents a subtype of the User type that includes only the id, name, and email properties. The logUserPublicInfo() function is modified to only log these three properties of the UserPublicInfo object.

Awaited<T>

The utility is used to mark a type as "unwrapped" when used in an await expression.

async function getData(): Promise<Awaited<string>> {
  const data = await fetch('https://sample.com/data');
  return data;
}

const data = getData();
console.log(data);

Readonly<T>

The Readonly<T> utility in TypeScript is used to create a read-only version of an object type. This means that the properties of the object cannot be modified.

type ReadonlyString = Readonly<string>;

const str: ReadonlyString = 'Hello, world!';

// This line will cause a type error because str is read-only
str = 'Hello, world!';

In this example, the ReadonlyString type represents a read-only version of the string type. The code is not allowed to reassign the value of the str variable because it is marked as read-only.

Typescript-type utilities are extremely useful in API testing scenarios. They enable you to write test code that is reusable, flexible, and type-safe, making it easier to maintain and less prone to errors. Record, Extract, Pick, Readonly, and Awaited are among the most often used type utilities in API testing. You may construct more efficient and successful test automation suites by using these types of utilities.

Did you find this article valuable?

Support Srinivasan Sekar by becoming a sponsor. Any amount is appreciated!