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 typeT
optional.Omit<T, K>
: Creates a new type that omits a specific set of propertiesK
from the typeT
.Exclude<T, U>
: Creates a new type that represents the set of values fromT
that are not assignable toU
.NonNullable<T>
: Creates a new type that represents the non-nullable version ofT
.Record<K, T>
: Creates a new type that represents a record of values with keys of typeK
and values of typeT
.Extract<T, U>
: Creates a new type that represents the intersection ofT
andU
, i.e., the set of properties that are present in bothT
andU
.Pick<T, K>
: Creates a new type that consists of a subset of the properties ofT
, specified by the keys in the union typeK
.Readonly<T>
: Makes all properties of a typeT
read-only.Awaited<T>
: Utility is used to mark a type as "unwrapped" when used in anawait
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.