Pure TypeScript
One line. Pure types. Done.
typia.assert<IBbsArticle>(article);Every other validator library in the JavaScript ecosystem makes you describe your data twice. typia doesnโt. The single line above is the entire validator definition. The type IBbsArticle is everything typia needs to know.
Why this matters
Most validator libraries canโt see TypeScript types โ at runtime, types are gone. So they ask you to re-encode the type in a form they can see:
class-validatorwants a class with stacked decorators. For NestJS apps, you usually maintain a third parallel definition for@nestjs/swagger. A typical request DTO ends up triple-duplicated.ajvwants a JSON Schema object. Hundreds of lines of{ "type": "...", "properties": {...} }for whatโs a 30-line TypeScript interface.zodwants you to build the schema with its own builder API (z.object({...})). The TypeScript type comes fromz.infer, so the schema is the source of truth โ your TypeScript types are downstream.
All three approaches share two problems:
- Drift. When you change a field on the TS side, nothing forces you to update the schema. The compiler canโt catch it. The mistake shows up only when production traffic hits.
- Effort. Mechanical, joyless typing of the same thing in two notations.
typia removes both. It is a compile-time transformer: it reads your TypeScript type at build time and writes the runtime code for you. The TypeScript type is the only source of truth.
Three approaches
The example below is the same data model โ a BBS article with files โ written for class-validator, ajv, and typia. Look at the highlighted files property in each.
undefined
import { ApiProperty } from "@nestjs/swagger";
import {
ArrayNotEmpty,
IsArray,
IsObject,
IsOptional,
IsString,
Matches,
MaxLength,
Type,
ValidateNested,
} from "class-validator";
export class BbsArticle {
@ApiProperty({
format: "uuid",
})
@IsString()
id!: string;
// Seven decorator calls just to say
// "files is either null or an array of AttachmentFile."
// Forget any one of them and you'll find out at runtime.
@ApiProperty({
type: () => AttachmentFile,
nullable: true,
isArray: true,
description: "List of attached files.",
})
@Type(() => AttachmentFile)
@IsArray()
@IsOptional()
@IsObject({ each: true })
@ValidateNested({ each: true })
files!: AttachmentFile[] | null;
@ApiProperty({
type: "string",
nullable: true,
minLength: 5,
maxLength: 100,
description: "Title of the article.",
})
@IsOptional()
@IsString()
title!: string | null;
@ApiProperty({
description: "Main content body of the article.",
})
@IsString()
body!: string;
@ApiProperty({
format: "date-time",
description: "Creation time of article",
})
@IsString()
created_at!: string;
}
export class AttachmentFile {
@ApiProperty({
type: "string",
maxLength: 255,
pattern: "^[a-zA-Z0-9-_]+$",
description: "File name.",
})
@Matches(/^[a-z0-9]+$/)
@MaxLength(255)
@IsString()
name!: string | null;
@ApiProperty({
type: "string",
nullable: true,
maxLength: 255,
pattern: "^[a-zA-Z0-9-_]+$",
description: "File extension.",
})
@Matches(/^[a-z0-9]+$/)
@MaxLength(8)
@IsOptional()
@IsString()
extension!: string | null;
@ApiProperty({
format: "url",
description: "URL of the file.",
})
@IsString()
url!: string;
}Same data. Same constraints. Three very different costs to maintain.
The typia version is a normal TypeScript interface. The validation rules live in type tags like tags.Format<"uuid"> and tags.MaxLength<255> that intersect with the base type. Rename files and TypeScript drags every reference along with it. Forget a constraint and you change the type once.
How can types validate at runtime?
Thatโs a fair objection. If types donโt exist at runtime, how does typia validate anything?
At compile time, typia rewrites your code. The typia.assert<T> call site is replaced inline with a hand-written validator specialized to T. There is no schema object at runtime because there is no general-purpose validator at runtime โ only the specific check your code needs.
This is called AOT (Ahead-of-Time) compilation. Look at what the compiler actually produces:
undefined
import typia from "typia";
import { IBbsArticle } from "./IBbsArticle";
export const assertArticle = typia.createAssert<IBbsArticle>();Thatโs hand-rolled JavaScript with no dynamic dispatch and no schema interpretation. Just a sequence of typeof checks and string comparisons that the V8 JIT loves.
Thatโs why typia is fast.
- Runtime validation: up to 20,000ร faster than
class-validator - JSON serialization: up to 200ร faster than
class-transformer
Measured on AMD Ryzen 9 7940HS, Rog Flow x13ย
Summary
- No schema files. You will not check a
*.schema.tsfile into your repo. - No drift. Rename a field โ every validator updates automatically because the type is the validator.
- No reflection. The runtime cost is whatever your compiled JavaScript already costs. Bundle it for the browser. Run it on the edge. It just works.
- Tags are real types.
tags.MaxLength<100>lives in the TypeScript type system. The compiler will flag you if you intersect it with the wrong base type.
Where to go next
- Setup โ install
ttsc(orts-patchfor TypeScript v6) and you can paste the code above into a fresh project right now. - Validators โ
is,assert,validate, and when to pick which. - Type Tags โ the full list of constraints you can intersect into a type.