TypeScript utility types for test automation: Partial and Omit
TypeScript comes with a variety of built-in utility types. Two of them - Partial and Omit - can be particularly useful in API tests.
Partial
Our backend services, web and mobile clients, and tests generate code from OpenAPI specifications with openapi-generator. This approach has numerous advantages, I described them a couple of years ago. To put it shortly, code generation saves time, reduces the risk of human error, and ensures consistency.
These models follow a specification, which often marks some path, query, or body parameters as required. The same notion exists in TypeScript:
type PostArticleRequest = {
title: string; // can't be undefined
summary: string; // can't be undefined
text?: string; // can be undefined
tags?: string[]; // can be undefined
};
To create an article, a client must send a title and a summary, while the other two properties are optional. Tests can build PostArticleRequest objects and pass them to the API layer:
async postArticle(body: PostArticleRequest): Promise<PostArticleResponse> {
// make a request with this body
}
But what if we wanted to validate that the server returns an error when a mandatory parameter is missing? The generated model prohibits that.
To make our API client more flexible, we can use Partial. It takes T and constructs a new type where all properties of T are optional.
type PartialPostArticleRequest = Partial<PostArticleRequest>;
/***
* Same as:
*
* type PartialPostArticleRequest = {
* title?: string | undefined;
* summary?: string | undefined;
* text?: string | undefined;
* tags?: string[] | undefined;
* }
***/
So if the API layer accepts Partial<PostArticleRequest> instead of plain PostArticleRequest, we are free to choose what parameters to send:
async postArticle(body: Partial<PostArticleRequest>): Promise<PostArticleResponse> {
// make a request with this body
}
await postArticle({
// don't set title
// add other props
})
This type is also useful for overrides in test data factories.
const requestFactory = {
postArticle(overrides?: Partial<PostArticleRequest>): PostArticleRequest {
return {
title: "random title",
summary: "random summary",
text: "random text",
tags: ["tag1", "tag2"],
...overrides,
};
},
};
// we don't need to pass summary if we only want to modify text
requestFactory.postArticle({ text: "modified text" });
Official documentation: https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype
Omit
Some requests can contain loads of properties, necessitating just as many nearly identical tests. Omit<T, K> can offer another solution! It takes T and constructs a new type, removing all K keys.
type OmitPostArticleRequest = Omit<PostArticleRequest, "title" | "summary">;
/***
* Same as:
*
* type OmitPostArticleRequest = {
* text?: string | undefined;
* tags?: string[] | undefined;
* }
***/
In a test, we can define an array of keys to exclude and make individual API calls without them:
const testData: PostArticleRequest = {
title: "random title",
summary: "random summary",
text: "random text",
tags: ["tag1", "tag2"],
};
const keys = ["title", "summary"];
const requestBody = (key: string) => {
const { [key as keyof PostArticleRequest]: _, ...restProps } = testData;
return restProps;
};
for (const key of keys) {
console.log(`Request body without ${key.toUpperCase()}:`, requestBody(key));
// fetch
// assert status code = 400
}
Output:
aleksei@debian:Blog
└── bun post.ts
Request body without TITLE: {
summary: "random summary",
text: "random text",
tags: [ "tag1", "tag2" ],
}
Request body without SUMMARY: {
title: "random title",
text: "random text",
tags: [ "tag1", "tag2" ],
}
Official documentation: https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys