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: AuthRequestreq: type AuthRequest = {
params: ParamsDictionary;
headers: IncomingHttpHeaders;
}
AuthRequest
,
res: AuthResponseres: type AuthResponse = {
send: Send<any, Response<any, Record<string, any>>>;
sendStatus: (code: number) => Response<any, Record<string, any>>;
}
AuthResponse
,
next: NextFunctionnext: NextFunction ) { if (req: AuthRequestreq.headers: IncomingHttpHeadersheaders.IncomingHttpHeaders.authorization?: string | undefinedauthorization?.String.replace(searchValue: string | RegExp, replaceValue: string): string (+3 overloads)replace("Bearer: ", "") === const staticKey: "f9asdjb28asfdlmx"staticKey) { const { const name: stringname = "user" } = req: AuthRequestreq.params: ParamsDictionaryparams; res: AuthResponseres.send: (body?: any) => Response<any, Record<string, any>>send(`Hello ${const name: stringname}`); } else { res: AuthResponseres.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: AuthResponseres: 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: ParamsDictionaryparams: {}, headers: IncomingHttpHeadersheaders: {}, }, const res: AuthResponseres, 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: AuthResponseres.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): voidtoHaveBeenCalledTimes(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: AuthResponseres.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): voidtoHaveBeenCalledWith(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.