You might want Pick not Partial in TypeScript
Partial
is a TypeScript utility-type that transforms all the property value types of an object to be optional. This can be useful when writing tests that only look at certain parts of an object. However, for TypeScript to be happy with passing said semi-populated object to a function, you must use the as
keyword to force TypeScript to see it as the full object. This is not ideal as you lose a degree of type safety in that your function thinks it has a full object but in fact does not. So, if you update your function to use other properties of the object, you may start getting runtime errors that could have been caught by TypeScript.
One solution I have been experimenting with is moving the process of scoping object parameters. So rather than using Partial
on the caller side, we can use Pick
— a TypeScript utility-type that reduces an object type to given properties — on the implementation side. Meaning we are telling TypeScript exactly what properties of a perhaps more complex object we intend to use.
If you have ever written Express middleware or similar, you may have come across this issue recreating req and res. So, if we are trying to test this middleware we only need to create a req and/or res object with the properties we need. Let’s look at an example:
// @filename: authenticate.ts
import { NextFunction, interface Request<P = core.ParamsDictionary, ResBody = any, ReqBody = any, ReqQuery = qs.ParsedQs, Locals extends Record<string, any> = Record<string, any>>
Request, interface Response<ResBody = any, Locals extends Record<string, any> = Record<string, any>>
Response } from "express";
const const staticKey: "f9asdjb28asfdlmx"
staticKey = "f9asdjb28asfdlmx";
export type type AuthRequest = {
params: ParamsDictionary;
headers: IncomingHttpHeaders;
}
AuthRequest = type Pick<T, K extends keyof T> = { [P in K]: T[P]; }
Pick<interface Request<P = core.ParamsDictionary, ResBody = any, ReqBody = any, ReqQuery = qs.ParsedQs, Locals extends Record<string, any> = Record<string, any>>
Request, "params" | "headers">;
export type type AuthResponse = {
send: Send<any, Response<any, Record<string, any>>>;
sendStatus: (code: number) => Response<any, Record<string, any>>;
}
AuthResponse = type Pick<T, K extends keyof T> = { [P in K]: T[P]; }
Pick<interface Response<ResBody = any, Locals extends Record<string, any> = Record<string, any>>
Response, "send" | "sendStatus">;
export default async function function authenticate(req: AuthRequest, res: AuthResponse, next: NextFunction): Promise<void>
authenticate(
req: AuthRequest
req: type AuthRequest = {
params: ParamsDictionary;
headers: IncomingHttpHeaders;
}
AuthRequest,
res: AuthResponse
res: type AuthResponse = {
send: Send<any, Response<any, Record<string, any>>>;
sendStatus: (code: number) => Response<any, Record<string, any>>;
}
AuthResponse,
next: NextFunction
next: NextFunction
) {
if (req: AuthRequest
req.headers: IncomingHttpHeaders
headers.IncomingHttpHeaders.authorization?: string | undefined
authorization?.String.replace(searchValue: string | RegExp, replaceValue: string): string (+3 overloads)
replace("Bearer: ", "") === const staticKey: "f9asdjb28asfdlmx"
staticKey) {
const { const name: string
name = "user" } = req: AuthRequest
req.params: ParamsDictionary
params;
res: AuthResponse
res.send: (body?: any) => Response<any, Record<string, any>>
send(`Hello ${const name: string
name}`);
} else {
res: AuthResponse
res.function sendStatus(code: number): Response<any, Record<string, any>>
sendStatus(403);
}
}
The beauty of using Pick
above is that we can still pass it a complete req/res object for example when passing it to app.get
or app.use
and TypeScript won’t complain. Now we can look at the advantage of this approach: we can construct objects with just the properties we need for each test:
// @filename: authenticate.test.ts
import function authenticate(req: AuthRequest, res: AuthResponse, next: NextFunction): Promise<void>
authenticate, { type AuthResponse = {
send: Send<any, Response<any, Record<string, any>>>;
sendStatus: (code: number) => Response<any, Record<string, any>>;
}
AuthResponse } from "./authenticate";
var test: jest.It
(name: string, fn?: jest.ProvidesCallback | undefined, timeout?: number | undefined) => void
test("unauthorized user", async () => {
const const res: AuthResponse
res: type AuthResponse = {
send: Send<any, Response<any, Record<string, any>>>;
sendStatus: (code: number) => Response<any, Record<string, any>>;
}
AuthResponse = {
send: Send<any, e.Response<any, Record<string, any>>>
send: jest.function jest.fn(): jest.Mock<any, any, any> (+1 overload)
fn(),
function sendStatus(code: number): e.Response<any, Record<string, any>>
sendStatus: jest.function jest.fn(): jest.Mock<any, any, any> (+1 overload)
fn(),
};
await function authenticate(req: AuthRequest, res: AuthResponse, next: e.NextFunction): Promise<void>
authenticate(
{
params: ParamsDictionary
params: {},
headers: IncomingHttpHeaders
headers: {},
},
const res: AuthResponse
res,
jest.function jest.fn(): jest.Mock<any, any, any> (+1 overload)
fn()
);
const expect: jest.Expect
<(code: number) => e.Response<any, Record<string, any>>>(actual: (code: number) => e.Response<any, Record<string, any>>) => jest.JestMatchers<(code: number) => e.Response<any, Record<string, any>>>
expect(const res: AuthResponse
res.function sendStatus(code: number): e.Response<any, Record<string, any>>
sendStatus).jest.Matchers<void, (code: number) => e.Response<any, Record<string, any>>>.toHaveBeenCalledTimes(expected: number): void
toHaveBeenCalledTimes(1);
const expect: jest.Expect
<(code: number) => e.Response<any, Record<string, any>>>(actual: (code: number) => e.Response<any, Record<string, any>>) => jest.JestMatchers<(code: number) => e.Response<any, Record<string, any>>>
expect(const res: AuthResponse
res.function sendStatus(code: number): e.Response<any, Record<string, any>>
sendStatus).jest.Matchers<void, (code: number) => e.Response<any, Record<string, any>>>.toHaveBeenCalledWith<[number]>(params_0: number): void
toHaveBeenCalledWith(403);
});
You could apply this to more than just testing. Anywhere you are accepting an object but only using a subset of properties could perhaps benefit from this approach. As with most things, it all depends on context, but it is worth keeping in mind that Pick
might be a better option than Partial
.