@TypedQuery
export function TypedQuery(): ParameterDecorator;
export namespace TypedQuery {
export function Body(): ParameterDecorator; // application/x-www-form-urlencoded body
export function Get(path?: string): MethodDecorator;
export function Post(path?: string): MethodDecorator;
export function Put(path?: string): MethodDecorator;
export function Patch(path?: string): MethodDecorator;
export function Delete(path?: string): MethodDecorator;
}Family of decorators for the URL query string.
@TypedQuery()— parameter decorator. Parses the query into a typed DTO, validates each field, coerces strings intonumber/boolean/bigint/null.@TypedQuery.Body()— parameter decorator forapplication/x-www-form-urlencodedrequest bodies. Same parsing semantics, different content type.@TypedQuery.${method}(path)— route decorator companion toTypedRoute.${method}. Pairs naturally with@TypedQuery.Body()for form-urlencoded POSTs.
Drop-in replacement for @Query() from @nestjs/common — but @Query() only sees strings; @TypedQuery() gives you the type your handler declared.
Basic usage — @TypedQuery()
import { TypedQuery, TypedRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import { tags } from "typia";
interface IListQuery {
page?: number & tags.Type<"uint32"> & tags.Minimum<1>;
limit?: number & tags.Type<"uint32"> & tags.Minimum<1> & tags.Maximum<100>;
sort?: "newest" | "oldest";
q?: string;
}
@Controller("articles")
export class ArticlesController {
@TypedRoute.Get()
public async list(
@TypedQuery() query: IListQuery,
): Promise<IPage<IArticle>> {
const page = query.page ?? 1;
const limit = query.limit ?? 20;
// …
}
}A request to GET /articles?page=2&limit=50&sort=newest calls the handler with { page: 2, limit: 50, sort: "newest" }. A request to GET /articles?page=abc returns 400 before the handler runs.
Things @TypedQuery() does that vanilla @Query() does not:
- Coerce
"42"→42when the field is typednumber. - Coerce
"true"/"false"→boolean. - Coerce
"null"→null, and"123"→123nwhen the field is typedbigint(BigInt()reads the digits;"123n"is rejected with 400). - Validate
tags.*constraints (Format,MinLength,Maximum, …). - Accept arrays via repeated keys (
?tag=ts&tag=node→tags: ["ts", "node"]).
Flat DTOs and Swagger decomposition
A query DTO is flat: every property is an atomic value (string, number, bigint, boolean), a literal or template literal, or an array of one of those, optionally | null. A nested object does not compile:
interface ISearchQuery {
filter?: {
section?: string;
};
}
// error TS(nestia.core.TypedQuery): unsupported type detected
// - ISearchQuery.filter: (__type | undefined)
// - nested object type is not allowed.Swagger documents each property of the DTO as its own query parameter by default, skipping the ones typia’s JSON schema leaves out, such as @hidden, @ignore, and @internal properties. Each parameter keeps what the property’s type says: tags.* constraints, the integer type of tags.Type<"int32">, the pattern of a template literal, the JSDoc description, @deprecated (except in Swagger 2.0, which has no such field), and its share of any @SwaggerExample.Parameter() example. To document the whole DTO as one query parameter with an object schema instead, spread into its keys by style: form and explode: true, set swagger.decompose to false in nestia.config.ts:
const config: INestiaConfig = {
input: () => NestFactory.create(AppModule),
output: "src/api",
swagger: {
output: "dist/swagger.json",
decompose: false,
},
};Swagger 2.0 has no object query parameter, so a Swagger 2.0 document decomposes the DTO whatever the setting says. Header objects are always decomposed too, because no OpenAPI version can spread one object into several headers. The runtime parsing is identical either way — decompose only affects the generated OpenAPI schema.
@TypedQuery.Body() — form-urlencoded bodies
import { TypedQuery, TypedRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import { tags } from "typia";
interface ILoginForm {
email: string & tags.Format<"email">;
password: string;
remember?: boolean;
}
@Controller("auth")
export class AuthController {
@TypedQuery.Post("login")
public async login(
@TypedQuery.Body() form: ILoginForm,
): Promise<{ token: string }> { ... }
}Use this for legacy HTML form posts and OAuth token endpoints where the body is application/x-www-form-urlencoded. For multipart/form-data (file uploads), use @TypedFormData.
When to use plain @Query instead
Reach for the vanilla decorator when:
- You need raw strings without coercion or validation (rare).
- A single route was working with
@Query()and you don’t want the migration churn.
@nestia/sdk still reads the declared type of a vanilla @Query() parameter into the SDK and the Swagger document, and holds it to the rules @TypedQuery() enforces at compile time, because the SDK has to write it as a query string: one object of statically named atomic, constant, or array-of-atomic properties, and a field-named @Query("key") of an atomic type or an array of them. A nested object, a dynamic key such as Record<string, string>, or a union of objects is reported as a diagnostic naming the type at fault and, inside an object, the property it sits at. Only the request goes through without coercion or validation.
Common pitfalls
Array params need repeated keys. ?tags=a,b,c does not parse as ["a","b","c"]. Send ?tags=a&tags=b&tags=c instead, or declare the field as string and split it yourself.
Trailing = for boolean. Browsers sometimes send ?remember (no value) to mean true. @TypedQuery() follows form-urlencoded semantics — declare remember: boolean and send ?remember=true.
Big numbers. Query strings carry no type information, so 42.0000001 round-trips as number. Use bigint and send ?x=42n if you need integer precision.
See also
- TypedBody — JSON request bodies.
- TypedFormData — multipart bodies (file uploads).
- TypedRoute — typed HTTP method decorators.