Skip to content

davidxhk/tstk

Repository files navigation

tstk

Type safety made simple.

tstk is a simple, minimal, and declarative runtime type-checking toolkit for TypeScript.

Just like its name suggests, it provides small but powerful utilities that help you narrow types easily, while handling all the type safety for you.

Why tstk?

🛡 TypeScript-style type safety at runtime

Check for a string or a string array with 1 line.

is(value, union("string", array("string"))) // value: string | string[]

Open in StackBlitz

tstk lets you write type validations that look almost exactly like TypeScript types with just a few simple and composable functions, helping you validate unknown data in a clean and maintainable way.

🔁 Schemas that look exactly like TypeScript

Define schemas just like how they look in TypeScript.

const PointSchema = [number, number] as const

type Point = Type<typeof PointSchema> // [number, number]

const UserSchema = { name: string, age: number }

type User = Type<typeof UserSchema> // { name: string, age: number }

Open in StackBlitz

  • Familiar and intuitive
    Define schemas exactly as you would in TypeScript

  • Automatic type inference
    Use Type<T> to get TypeScript types from schemas

  • Compatible with existing tools
    Works with Standard Schema V1, so you can use it alongside Zod, Valibot, ArkType, and more

📏 Type safety that fits in your pocket

tstk has 0 dependencies and a featherweight minzipped size.

npm version npm package minimized gzipped size

It's tiny enough so you can use it anywhere, without worrying about its impact on your bundle size.

If you need type-safe runtime validation without the overhead, tstk is built just for you.

Install

Use your preferred package manager to install 🧰tstk from the npm registry.

npm install tstk
yarn add tstk
pnpm add tstk

Usage

tstk allows you to check for a wide variety of types including primitives, literals, classes, objects, records, tuples, arrays, unions, intersections, schemas, and even standard schemas.

Primitives

Primitive types are represented by their literal strings such as "string" or "number".

Notably, "object" includes functions and excludes null (unlike typeof).

is({}, "object") // true
is([], "object") // true
is(() => {}, "object") // true
is(null, "object") // false

Also, "record" matches plain objects only.

is({}, "record") // true
is([], "record") // false
is(() => {}, "record") // false
is(null, "record") // false
Show examples
is(value, "string") // value: string

is(value, "number") // value: number

is(value, "bigint") // value: bigint

is(value, "boolean") // value: boolean

is(value, "symbol") // value: symbol

is(value, "object") // value: object

is(value, "record") // value: Record<keyof any, unknown>

is(value, "array") // value: readonly unknown[]

is(value, "function") // value: (...args: unknown[]) => unknown

is(value, "any") // value: any

is(value, "null") // value: null

is(value, "undefined") // value: undefined

Open in StackBlitz

Literals

Literal types are represented by their literal values.

Any literal string, number, bigint, boolean, symbol, or null is supported.

To match a literal primitive type like "string" or "number", use literal(type).

Show examples
is(value, "") // value: ""

is(value, "foo") // value: "foo"

is(value, 0) // value: 0

is(value, 42) // value: 42

is(value, 0n) // value: 0n

is(value, 983498124981598n) // value: 983498124981598n

is(value, true) // value: true

is(value, false) // value: false

const $a = Symbol("a")
is(value, $a) // value: typeof $a

const $b = Symbol.for("b")
is(value, $b) // value: typeof $b

is(value, null) // value: null

is(value, literal("string")) // value: "string"

is(value, literal("number")) // value: "number"

Open in StackBlitz

Classes

Classes match their instances. Native classes like Date are supported too.

Show examples
class MyClass {}
is(value, MyClass) // value: MyClass

is(value, Date) // value: Date

is(value, RegExp) // value: RegExp

is(value, Map) // value: Map<unknown, unknown>

is(value, Set) // value: Set<unknown>

is(value, Promise) // value: Promise<unknown>

Open in StackBlitz

Objects

Objects match plain objects with the given properties and types.

optional(type) marks a property as optional.

is({ foo: 1 }, { foo: optional("number") }) // true
is({}, { foo: optional("number") }) // true

readonly(type) marks a property as readonly.

const value = { foo: 1 }
is(value, { foo: readonly("number") }) // false

Object.freeze(value)
is(value, { foo: readonly("number") }) // true

By default, properties must be exact. Pass false to allow non-specified properties.

is({ foo: 1, bar: 2 }, { foo: "number" }) // false
is({ foo: 1, bar: 2 }, { foo: "number" }, false) // true
Show examples
is(value, { id: "number", name: "string" }) // value: { id: number, name: string }

is(value, { id: "number", name: optional("string") }) // value: { id: number, name?: string | undefined }

is(value, { theme: "string" }) // value: { theme: string }

is(value, { theme: readonly("string") }) // value: { readonly theme: string }

is(value, { username: "string" }) // value: { username: string }

is(value, { username: "string" }, false) // value: { username: string } (exact=false)

Open in StackBlitz

Records

wip

A collective record such as record("string", "number") checks that every prop matches props.

is(value, record("string", "number")) // value: Record<string, number>

A concrete record such as record(["foo", "bar"], "number") checks that all props are present.

is(value, record(["foo", "bar"], "string")) // value: Record<"foo" | "bar", string>

partial(record) works with concrete records or schemas only.

const Foo = partial(record(["foo"], "number"))
type FooType = Type<typeof Foo> // FooType: { foo?: number | undefined }

const Bar = partial({ bar: number })
type BarType = Type<typeof Bar> // BarType: { bar?: number | undefined }

Tuples

Tuples match fixed-length arrays with the given types in each position.

Show examples
is(value, ["string", "number"]) // value: [string, number]

is(value, ["number", "number", "number"]) // value: [number, number, number]

is(value, ["object", "function"]) // value: [object, (...args: unknown[]) => unknown]

is(value, [Date, Date]) // value: [Date, Date]

Open in StackBlitz

Arrays

array(type) matches arrays of any length with the given type.

Show examples
is(value, array("number")) // value: number[]

is(value, array("string")) // value: string[]

is(value, array(Date)) // value: Date[]

is(value, array(union(0, 1))) // value: (0 | 1)[]

is(value, array(union("foo", "bar", "baz"))) // value: ("foo" | "bar" | "baz")[]

is(value, array({ action: "string", payload: "any" })) // value: { action: string, payload: any }[]

Open in StackBlitz

Unions

union(...types) represents a type union.

Show examples
is(value, union("string", "number")) // value: string | number

is(value, union("string", array("string"))) // value: string | string[]

is(value, union("string", "number", "symbol")) // value: string | number | symbol

is(value, union("foo", "bar", "baz")) // value: "foo" | "bar" | "baz"

is(value, union("number", null)) // value: number | null

is(value, union("boolean", "true", "false", 0, 1)) // value: boolean | 0 | 1 | "true" | "false"

Open in StackBlitz

Intersections

merge(...objects) combines object types with a shallow merge.

Alternatively, joint(...types) defines an intersection of any type.

Show examples
is(value, merge({ name: "string" }, { age: "number" })) // value: { name: string, age: number }

is(value, merge({ id: "number" }, { email: optional("string") })) // value: { id: number, email?: string | undefined }

is(value, joint({ user: { name: "string" } }, { user: { age: "number" } })) // value: { user: { name: string } & { age: number } }

Open in StackBlitz

Schemas

You can define schemas with any type.

To improve typing, use wrappers like primitive(type) and literal(type) or predicates like string and number.

import { boolean, literal, number, optional, primitive, string } from "tstk"

const UserSchema = {
  // Wrappers
  type: literal("User"),
  name: primitive("string"),
  admin: optional("boolean"),

  // Predicates
  email: string,
  age: number,
  deleted: boolean,
}

Use Type<T> to infer a schema's type.

import type { Type } from "tstk"

type User = Type<typeof UserSchema>
type User = {
  type: "User"
  name: string
  admin?: boolean | undefined
  email: string
  age: number
  deleted: boolean
}
Show examples
const UserSchema = {
  userid: string,
  name: string,
  age: number,
  email: string,
  deleted: boolean,
}

const AddressSchema = record(["street", "city", "zipcode", "country"], "string")

const SettingsSchema = {
  theme: union("light", "dark"),
  notifications: partial(record(["email", "sms"], "boolean")),
}

const RoleSchema = union("admin", "editor", "viewer")

const PostSchema = {
  id: string,
  title: string,
  body: string,
  attachment: optional("string"),
  publishedAt: number,
  tags: array("string"),
}

const FriendSchema = merge(
  pick(UserSchema, ["userid", "name"]),
  { startedAt: "number" },
)

const ProfileSchema = {
  user: UserSchema,
  address: AddressSchema,
  settings: SettingsSchema,
  roles: array(RoleSchema),
  posts: array(PostSchema),
  friends: array(FriendSchema),
}

Open in StackBlitz

Standard Schemas

Standard schemas are supported too.

They can even be composed with other types like array or union.

Show examples
const UserSchema = z.object({
  name: z.string(),
  age: z.number()
})

is(value, UserSchema) // value: { name: string, age: number }

is(value, array(UserSchema)) // value: { name: string, age: number }[]

is(value, union("string", UserSchema)) // value: string | { name: string, age: number }

Open in StackBlitz

API

Core Functionality

is(value, type, exact?)
Check if value matches type, allowing extra properties if exact is false.

has(value, prop, type?, exact?)
Check if value has property prop that matches some optional type, allowing extra properties if exact is false.

assert(condition, message)
Throw an error with message if condition is false.

Type Descriptors

primitive(type)
Define a primitive type such as "string" or "number".

literal(type)
Define a literal type such as literal("hello") or literal(42).

union(...types)
Define a union type that matches one of types.

joint(...types)
Define a joint type that matches all of types.

array(type)
Define an array type where every element matches type.

tuple(...types)
Define a tuple type matching types in length and types.

record(props, type)
Define a record type that matches a plain object with props, where all values match type.

partial(record)
Convert all properties of record to optional.

optional(type)
Define an optional property that matches type.

readonly(type)
Define a readonly property that matches type.

string(value)
Check if value is a string.

number(value)
Check if value is a number.

bigint(value)
Check if value is a bigint.

boolean(value)
Check if value is a boolean.

symbol(value)
Check if value is a symbol.

object(value)
Check if value is an object.

function_(value)
Check if value is a function.

any(value)
This is effectively a no-op.

json(value)
Check if value is a JSON value.

propertyKey(value)
Check if value is a property key.

Utility Functions

get(object, prop)
Get the value of prop for object, binding functions if applicable.

keys(object)
Get all property keys of object, casting to integers if applicable.

filter(array, type)
Return a new array including only elements that match type.

reject(array, type)
Return a new array excluding elements that match type.

pick(object, props)
Return a new object including only props from the original.

omit(object, props)
Return a new object excluding props from the original.

remap(object, mapping)
Return a new object whose keys are remapped using mapping.

merge(...objects)
Return a new object merged from all objects, with last taking precedence.

Contributing

Contributions, issues, and feature requests are welcome!

  1. Fork the repository.
  2. Create your feature branch:
    git checkout -b my-new-feature
  3. Commit your changes:
    git commit -am 'My feature'
  4. Push to the branch:
    git push origin my-new-feature
  5. Submit a PR.

Please submit your feedback, suggestions, and bug reports on the issues page.

License

MIT © David Xie

Acknowledgments

Inspired by 🎆type-fest and 🛠️lodash.

If tstk helps you, star the repo or share it with your team!

Happy type checking!

Maintained with ❤️ from 🇸🇬.

About

Type safety made simple.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published