TypeScript icon, indicating that this package has built-in type declarations

1.9.0 • Public • Published

cat.ts logo


Compose validation parsers with static type inference; or
Auto-infer parsers from sample values
cast.ts makes it easy to handle data from RESTful API

npm Package Version Minified Package Size Minified and Gzipped Package Size

Inspired by Zod and tRPC with automatic type conversion, type reflection, sample values and auto-infer from sample value.

Feature Highlights

  • Explicit type conversion
  • Right-to-the-point error message
  • Static type inference
  • Composable: builder functions (i.e. optional()) return new parser instance
  • Convenience: support auto-infer parser from from sample value
  • Safe: Parse, don't type-check
  • Tiny: below 2kB minizipped
  • Zero dependencies
  • Isomorphic Package: works in Node.js and browsers
  • Works with plain Javascript, Typescript is not mandatory
  • Extensible: support meta-programming with type reflection and sample values


cast.ts is an isomorphic package, it runs in both node.js and browsers.

You can use cast.ts to check against request data on the server, and also response data on the client. This double-checking approach add a safe layer between the interface (API) of separately implemented frontend and backend for easier debugging with specific error message and better security and maintainability.

Bonus: cast.ts also supports static type inference in Typescript project.


npm install cast.ts

You can also install cast.ts with pnpm, yarn, or slnpm

Usage Example

You can compose a parser by composing a wide range of parser builders, or auto infer a parser from sample value.

Composing Parsers

import { optional, object, int, array, id, string } from 'cast.ts'

let searchQuery = object({
  page: optional(int({ min: 1 })),
  count: optional(int({ max: 25 })),
  cat: optional(array(id(), { maybeSingle: true })),
  keyword: string({ minLength: 3 }),

type SearchQuery = ParseResult<typeof searchQuery>
// the inferred type of parse result will be like below
type SearchQuery = {
  page?: number
  count?: number
  cat: number[]
  keyword: string

// Example: http://localhost:8100/product/search?page=2&count=20&keyword=food&cat=12&cat=18
app.get('/product/search', async (req, res) => {
  // query is validated with inferred type
  let query = searchQuery.parse(req.query)
  // { page: 2, count: 20, cat: [ 12, 18 ], keyword: 'food' }

Noted that the parsed page, count are numbers, and the cat is array of numbers, instead of being string and array of strings in the original req.query from express router.

If the validation is not successful, the parser will throw an InvalidInputError. You can surround the call with try-catch to response specific error message to the client.

A full list of built-in parsers are documented in the Supported Parsers section below.

For more complete example, see examples/server.ts

Infer from Sample Value

You can use inferFromSampleValue() to auto-infer the parser based on given sample value.

Usage example:

import { inferFromSampleValue } from 'cast.ts'

let parser = inferFromSampleValue({
  postList: [
      id: 1,
      title: 'Hello World',
      type$enums: ['public', 'vip'],
      hidden$optional: true,
let input = parser.parse(req.body)
/* the type of parsed input is inferred as:
  postList: Array<{
    id: number
    title: string
    type: 'public' | 'vip'
    hidden?: boolean

Supported field name decorators (suffix): $enums (alias: $enum), $nullable (alias: $null), $optional (alias: ?).

The field name decorators can be used in combination in any order.

Supported Parsers

Parser Types and Usage Examples

Utility type:

// to extract inferred type of parsed payload
type ParseResult<T extends Parser<R>, R = unknown> = ReturnType<T['parse']>

Reference types:

type Parser<T> = {
  parse(input: unknown, context?: ParserContext): T
  type: string // typescript signature of parsed value
  sampleValue: T
  randomSample: () => T

// used when building new data parser on top of existing parser
type ParserContext = {
  // e.g. array parser specify "array of <type>"
  typePrefix?: string
  // e.g. array parser specify "<reason> in array"
  reasonSuffix?: string
  // e.g. url parser specify "url" when calling string parser
  overrideType?: string
  // e.g. object parser specify entry key when calling entry value parser
  name?: string

For custom parsers:

If you want to implement custom parser you may reuse the InvalidInputError error class. The argument options is listed below:

class InvalidInputError extends Error {
  status: number // alias of statusCode
  statusCode: number // default 400
  constructor(options: InvalidInputErrorOptions) {
    let message = '...'
type InvalidInputErrorOptions = {
  name: string | undefined
  typePrefix: string | undefined
  reasonSuffix: string | undefined
  expectedType: string
  reason: string

In addition, you may use the populateSampleProps helper function when constructing custom parser. The type signature is listed below:

function populateSampleProps<T>(options: {
  defaultProps: SampleProps<T>
  customProps?: CustomSampleOptions<T>
}): SampleProps<T>

type SampleProps<T> = {
  sampleValue: T
  randomSample: () => T

type CustomSampleOptions<T> = {
  sampleValue?: T
  sampleValues?: T[]
  randomSample?: () => T


Usage Example:

// keyword is a string potentially being empty
let keyword = string().parse(req.query.keyword)

// username is an non-empty string
let username = string({ minLength: 3, maxLength: 32 }).parse(req.body.username)

Options of string parser:

type StringOptions = {
  nonEmpty?: boolean
  minLength?: number
  maxLength?: number
  match?: RegExp
  trim?: boolean // default true



// score is a non-NaN number
let score = number().parse(req.body.score)

// height is a non-negative number
let height = number({ min: 0 }).parse(req.body.height)

Options of number parser:

type NumberOptions = {
  min?: number
  max?: number



// degree is a real number (non-NaN, non-infinite)
let degree = float().parse(

// height is a real number with at most 2 digits after the decimal point
let height = float({ toFixed: 2 }).parse(req.body.height)

// weight is a real number with at most 3 significant digits
let weight = float({ toPrecision: 2 }).parse(req.body.weight)

// score is a real number between 0 and 100 inclusively
let score = float({ min: 0, max: 100 })

Options of number parser:

type FloatOptions = NumberOptions & {
  toFixed?: number
  toPrecision?: number


Usage Example:

// score is an integer between 1 to 5
let rating = int({ min: 1, max: 5 }).parse(req.body.rating)

Options of int parser: Same as NumberOptions


Usage Example:

// cat_id is a non-zero integer
let cat_id = id().parse(

The id parser doesn't take additional options


It parse all truthy values as true, and falsy value as false with some exceptions to better support html form.

Example truthy value:

  • "on"
  • "true"
  • non-empty string (after trim)
  • non-zero numbers

Example falsy value:

  • "false"
  • 0
  • NaN
  • null
  • undefined
  • ""
  • " "
  • "\t"
  • "\r"
  • "\n"


// is_admin is a boolean value
let is_admin = boolean().parse(user.is_admin)

// is_cancelled will be false if product.cancel_time is null
let is_cancelled = boolean().parse(product.cancel_time)

// effectively asserting the user is admin (throw InvalidInputError if user.is_admin is falsy)

Options of number parser:

function boolean(expectedValue?: boolean): Parser<boolean>


When this parser is used as a field of object parser, it will treat absent fields as falsy because browsers will omit unchecked fields when submitting form.


// is_admin is a boolean
let is_admin = checkbox().parse(req.body.is_admin)

The checkbox parser doesn't take additional options



// primary_color is a string in "#rrggbb" format
let primary_color = color().parse(req.body.primary_color)

The color parser doesn't take additional options



// newUser is an object of { username: string, email: string }
let newUser = object({
  username: string({ minLength: 3, maxLength: 32 }),
  email: email(),

Options of object parser:

type ObjectOptions<T extends object> = {
  [P in keyof T]: Parser<T[P]>



// sinceDate is a Date object indicating a timestamp in the past
let sinceDate = date({ max: }).parse(req.query.sinceDate)

// untilDate is a Date object between sinceDate and current timestamp
let untilDate = date({
  min: sinceDate,

Options of date parser:

type DateOptions = {
  min?: number | Date | string
  max?: number | Date | string


Convert from string | Date | number to string in the format of yyyy-mm-dd


// sinceDate is date string indicating a date in the past
let sinceDate = dateString({ max: }).parse(req.query.sinceDate)

// untilDate is a date string between sinceDate and current date
let untilDate = date({
  min: sinceDate,

Options of dateString parser:

type DateStringOptions = {
  nonEmpty?: boolean
  min?: number | Date | string
  max?: number | Date | string


Convert from string | Date | number to string in the format of hh:mm


// sinceTime is time string indicating a time in the past (same date)
let sinceTime = timeString({ max: }).parse(req.query.sinceTime)

// untilTime is a time string between sinceTime and current time
let untilTime = time({
  min: sinceTime,

Options of timeString parser:

type TimeStringOptions = {
  nonEmpty?: boolean
  min?: number | Date | string
  max?: number | Date | string



// blogUrl is a string of hyperlink
let blogUrl = url({ protocols: ['https', 'http'] }).parse(req.body.blogUrl)

Options of url parser:

type UrlOptions = StringOptions & {
  domain?: string
  protocol?: string
  protocols?: string[]



// userEmail is a string of email address
let userEmail = email().parse(

Options of url parser:

type EmailOptions = StringOptions & {
  domain?: string



// effectively asserting the role is 'guest' (throw InvalidInputError if it isn't)
let role = literal('guest').parse(req.session?.role)

Options of literal parser:

function literal<T>(value: T): Parser<T>

Values / Enums


// color is like an enums value of 'red' | 'yellow' | 'green' | 'blue'
let color = values([
  'red' as const,
  'yellow' as const,
  'green' as const,
  'blue' as const,

Options of values parser:

function values<T>(values: T[]): Parser<T>

The function values() is also aliased as enums()



// categories is an array of string
let categories = array(string()).parse(req.body.categories)

// req.query is a string or array of string
// item_ids is an array of string
let item_ids = array(string(), { maybeSingle: true }).parse(req.query.item_id)

Options of array parser:

type ArrayOptions = {
  minLength?: number
  maxLength?: number
  maybeSingle?: boolean // to handle variadic value e.g. req.query.category



// tag is a string or null value
let tag = nullable(string()).parse(req.body.tag)

Options of nullable parser:

function nullable<T>(parser: Parser<T>): Parser<T | null>



/** searchQuery is an object of {
 *   page?: number
 *   count?: number
 *   category?: string
 *   keyword: string
 * }
let searchQuery = object({
  page: optional(int({ min: 1 })),
  count: optional(int({ max: 25 })),
  category: optional(string()),
  keyword: string({ minLength: 3 }),

Options of nullable parser:

function optional<T>(parser: Parser<T>): Parser<T | undefined>


The API design is inspired by Zod. The main difference is cast.ts auto convert data between different types with it's valid, e.g. it converts numeric value from string if it's valid, which is useful when parsing data from req.query.

The icon of cast.ts is generated with diffuse-the-rest and up-scaled by Real-ESRGAN, then it is post-processed with GIMP.


This project is licensed with BSD-2-Clause

This is free, libre, and open-source software. It comes down to four essential freedoms [ref]:

  • The freedom to run the program as you wish, for any purpose
  • The freedom to study how the program works, and change it so it does your computing as you wish
  • The freedom to redistribute copies so you can help others
  • The freedom to distribute copies of your modified versions to others

Package Sidebar


npm i cast.ts

Weekly Downloads






Unpacked Size

93.1 kB

Total Files


Last publish


  • beenotung