Hi,
Just wondering what the best practice for using httpEventNormalizer and jsonBodyParser with typescript are?
For example if I add httpEventNormalizer i know there will be a pathParamaters object on my event so shouldnt have to check for null object in my code (one of the benefits of the middleware) however as soon as i remove my null check i get a type error in my code "Object is possibly 'null'"
Similarly with jsonbodyparser typescript is still expecting the body to be a string rather than an object
Im sure i am missing something obvious and massively appreciate any thoughts on this.
Kind regards, Ed
Ran into the same issue, not sure if below is the best way to go, and wether there could be a type provided by this middleware.
@ed-sparkes You could extend the original AWSLambda.APIGatewayProxyEvent like this:
import middy from "@middy/core";
import httpErrorHandler from "@middy/http-error-handler";
import httpEventNormalizer from "@middy/http-event-normalizer";
interface APIGatewayProxyEventMiddyNormalised extends AWSLambda.APIGatewayProxyEvent {
queryStringParameters: { [name: string]: string };
multiValueQueryStringParameters: { [name: string]: string[] };
pathParameters: { [name: string]: string };
}
const handler = middy(async function getSomething(
event: APIGatewayProxyEventMiddyNormalised,
context: AWSLambda.Context
): Promise<AWSLambda.APIGatewayProxyResult> {
if (!event.pathParameters.id) {
throw new Error("Please pass an id.");
}
return { statusCode: 200, body: 'ok' };
});
});
handler.use(httpErrorHandler()).use(httpEventNormalizer());
export { handler };
Maybe it would be even easier with NonNullable
It becomes more interesting if you're also using the validator and httpJsonBodyParser middleware, since then you can assume event.body is present and parsed in an object with a specific type (instead of a string as the default awslambda type), yet if you don't change the event type it will still require you to check for null, and parse it even if it's already a parsed object.
What I end up doing is:
export interface APIGatewayProxyEventMiddyNormalised<T = string> extends Omit<AWSLambda.APIGatewayProxyEvent, "body"> {
queryStringParameters: NonNullable<AWSLambda.APIGatewayProxyEvent["queryStringParameters"]>;
multiValueQueryStringParameters: NonNullable<AWSLambda.APIGatewayProxyEvent["multiValueQueryStringParameters"]>;
pathParameters: NonNullable<AWSLambda.APIGatewayProxyEvent["pathParameters"]>;
body: T;
}
since also using @middy/validator and @middy/http-json-body-parser in some places. This does feel wrong, but it's what's happening when using that middleware. Hopefully someone with more TS experience can point the way to a better solution?
Hello @larrybolt, thanks a lot for your comments. Although your ideas are not solving the problem in a perfect way I feel they do help to ease the pain with Typescript types.
Unfortunately, most of the people in the Middy's core team are not experienced enough with Typescript to have figured out a solution for this.
I wonder if there's any way to provide some sort of trait system where we can make so that all the mutations applied by middlewares to event and context can be recorded as an additional trait that gets attached (or can be manually attached) to the event and context definition within the handler.
Would you like to investigate this option more?
@lmammino I'll take a look at it, but I can't make any promises. Digging around in typescripts utility types and the utility-types library does make it seem like it would be very doable, for instance a "trait" that could be used to make the event's body parameter a type instead of string:
import { Assign } from "utility-types";
type SetBodyToType<A extends object, B extends object> = Assign<A, Record<"body", B>>;
interface OwnType {
name: string;
}
const handler = middy(async function(
event: SetBodyToType<AWSLambda.APIGatewayProxyEvent, OwnType>
) {
const user: OwnType = event.body;
return { statusCode: 200, message: `Hello ${user.name}!` };
});
handler
.use(validator({ inputSchema }))
.use(httpJsonBodyParser());
export { handler };
Is something like this what you had in mind?
Thanks a lot, @larrybolt for taking the time to investigate this a bit further.
This seems (to me!) a bit overly complicated but it could work as well :)
I was thinking more something like this, using intersection types:
interface OwnEvent {
body: {
name: string
}
}
const handler = middy(async function(
event: AWSLambda.APIGatewayProxyEvent & OwnEvent
) {
const user: OwnType = event.body;
return { statusCode: 200, message: `Hello ${user.name}!` }
});
handler
.use(validator({ inputSchema }))
.use(httpJsonBodyParser())
export { handler }
I never tried this in practice, so I have no real clue whether it could work and if it would satisfy most use cases... I feel this could work for added fields, but I have no idea what happens in the case of "conflicts" between declarations. In my example, I am assuming that the last type in the intersection prevails and that nested declaration are somehow "merged".
What do you think?
While it works using the intersection would be incorrect in the case of validate & jsonBodyParser because event.body is no longer a string.
By using the intersection we're allowing it to be both a string or object:

Which means if you add httpJsonBodyParser and change the type in a refactor, TS won't complain if you don't also change the code in the hander to access event.body as an object instead of as a string:

On the other hand with these types we're dependent on the user not forgetting to change the event type 馃し鈥嶁檧
In an ideal world the type of the handler would be changed by the applied middleware, but I don't think that's possible :P
Thanks a lot for taking the time to test this. I honestly don't have other ideas as of now :(
I guess this is because of the dynamic nature of the chain of responsibility pattern. I would have to check other similar frameworks to see what kind of strategies did they adopt to deal with this.
No problem! :D Love helping out! Thanks for taking time to look into types in the first place!
I think as long as the type is not in the way, but helps you (like the second example in the last snippet) it's fine and similar to what the redux types do.
And it's up to the developer to make sure you type their stuff correctly (A typescript thing in general it seems and the result of having types being optional). But I could of course be missing something!
Awesome, thanks @larrybolt.
Regarding your last snippet. Do you think the current typescript support can be expanded somehow to provide some primitives that can be then composed by using the Assign helper or other utility types?
Yeah probably :D I'll look into it!
TS docs have been added to v2 README!
Most helpful comment
Thanks a lot, @larrybolt for taking the time to investigate this a bit further.
This seems (to me!) a bit overly complicated but it could work as well :)
I was thinking more something like this, using intersection types:
I never tried this in practice, so I have no real clue whether it could work and if it would satisfy most use cases... I feel this could work for added fields, but I have no idea what happens in the case of "conflicts" between declarations. In my example, I am assuming that the last type in the intersection prevails and that nested declaration are somehow "merged".
What do you think?