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.
Check for a string or a string array with 1 line.
is(value, union("string", array("string"))) // value: string | string[]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.
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 }-
Familiar and intuitive
Define schemas exactly as you would in TypeScript -
Automatic type inference
UseType<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
tstk has 0 dependencies and a featherweight minzipped 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.
Use your preferred package manager to install 🧰tstk from the npm registry.
npm install tstkyarn add tstkpnpm add tstktstk 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.
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") // falseAlso, "record" matches plain objects only.
is({}, "record") // true
is([], "record") // false
is(() => {}, "record") // false
is(null, "record") // falseShow 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: undefinedLiteral 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"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>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") }) // truereadonly(type) marks a property as readonly.
const value = { foo: 1 }
is(value, { foo: readonly("number") }) // false
Object.freeze(value)
is(value, { foo: readonly("number") }) // trueBy 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) // trueShow 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)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 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]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 }[]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"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 } }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),
}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 }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.
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.
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.
Contributions, issues, and feature requests are welcome!
- Fork the repository.
- Create your feature branch:
git checkout -b my-new-feature
- Commit your changes:
git commit -am 'My feature' - Push to the branch:
git push origin my-new-feature
- Submit a PR.
Please submit your feedback, suggestions, and bug reports on the issues page.
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 🇸🇬.