Using unsafe cast in TypeBox to convert strings to template string literals
1 September 2023
tl;dr
You can use Type.Unsafe
to change the output type of a schema such as to cast a formatted string
to a template literal type in a validation schema:
type UUID = `${string}-${string}-${string}-${string}-${string}`;
const schema = {
id: Type.Unsafe<UUID>(Type.String({ format: "uuid" })),
};
I wanted to share this explanation for anyone trying to figure out how to do this, because it took me a while to figure it out!
The scenario is I'm using TypeBox as a type provider for Fastify which is using Ajv schema under the hood for parsing and validation of request params. My route handler is expecting a UUID string parameter:
import { UUID } from "crypto";
fastify.get(
"/entity/:id",
{
schema: {
params: Type.Object({
id: Type.String({ format: "uuid" }),
}),
},
},
async (request, reply) => {
const id: UUID = request.params.id;
}
);
The UUID type imported from the crypto module is a template string literal type:
type UUID = `${string}-${string}-${string}-${string}-${string}`;
The code example fails because request.params.id
does not get cast to the type UUID despite being validated using { format: "uuid" }
in the schema.
We could do the following:
async (request, reply) => {
const id: UUID = request.params.id as UUID;
}
However this doesn't work in every case such as if the type of request.params
is a union for example:
fastify.get(
"/:kind/:id",
{
schema: {
params: Type.Union([
Type.Object({
kind: Type.Literal("a"),
id: Type.String(),
}),
Type.Object({
kind: Type.Literal("b"),
id: Type.String({ format: "uuid" }),
}),
])
},
},
async (request, reply) => {
handleParams(request.params);
}
);
function handleParams(params: { kind: "a"; id: string } | { kind: "b"; id: UUID }) {
}
There is no longer a place to put the as UUID
as we did before.
The solution I found here is to use Type.Unsafe
to do an unsafe cast of the type in the schema:
id: Type.Unsafe<UUID>(Type.String({ format: "uuid" })),
Now the type of request.params.id
will be UUID
- or in the union case the type of params is { kind: "a"; id: string } | { kind: "b"; id: UUID }
as we would expect.