tstk
v1.0.8
Published
Type safety made simple.
Downloads
20
Readme
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?
Neat
Tired of creating a schema for every single shape and size? Inline your type definitions with simple, composable functions like is, array, record, and union. Never hit F12 on your keyboard (Go to definition) again.
Easy
Checking for a string or a string array? Here you go: union("string", array("string")). Define your types with descriptors that mimic TypeScript as closely as possible. tstk handles the rest for you.
Tiny
With zero dependencies and a featherweight minzipped size, tstk keeps your bundle small. So you can install it guilt-free and ease your bundlephobia.
If you need a handy and lightweight approach to runtime validation, tstk is built just for that.
Install
Use your preferred package manager to install 🧰tstk from the npm registry.
npm install tstkyarn add tstkpnpm add tstkQuick Example
import { array, is, union } from "tstk"
const value = JSON.parse("['hello', 42, 'world']")
if (is(value, array(union("string", "number")))) {
value
/**
┌──────────────────────────────────┐
│ const value: (string | number)[] │
└──────────────────────────────────┘
*/
}Use Cases
1. Validating API Responses
For simple API responses, tstk cuts out the need to define and parse against a full schema, enabling clean and inline validation that's easier to maintain and integrate into your data flow.
- With zod
import { z } from "zod" const UserSchema = z.object({ id: z.number(), name: z.string(), }) fetch("/api/users") .then(res => res.json()) .then((data) => { const result = UserSchema.safeParse(data) if (result.success) { result.data /** ┌───────────────────────────────────────────────┐ │ (property) data: { id: number; name: string } │ └───────────────────────────────────────────────┘ */ } }) - With tstk
import { is } from "tstk" fetch("/api/users") .then(res => res.json()) .then((data) => { if (is(data, { id: "number", name: "string" })) { data /** ┌──────────────────────────────────────────┐ │ const data: { id: number; name: string } │ └──────────────────────────────────────────┘ */ } })
2. Validating Dynamic Query Parameters
When working with runtime data like URL query parameters in a Next.js application, tstk offers a direct and minimalistic approach to validation without the extra overhead of creating a schema.
- With zod
import { useSearchParams } from "next/navigation" import { z } from "zod" const QuerySchema = z.object({ id: z.string(), }) function MyComponent() { const searchParams = useSearchParams() const query = Object.fromEntries(searchParams.entries()) const result = QuerySchema.safeParse(query) if (result.success) { result.data /** ┌─────────────────────────────────┐ │ (property) data: { id: number } │ └─────────────────────────────────┘ */ } } - With tstk
import { useSearchParams } from "next/navigation" import { is } from "tstk" function MyComponent() { const searchParams = useSearchParams() const query = Object.fromEntries(searchParams.entries()) if (is(query, { id: "string" })) { query /** ┌─────────────────────────────┐ │ const query: { id: string } │ └─────────────────────────────┘ */ } }
3. Validating Local Storage Data
For data from sources like local storage where the shape isn't known until runtime, tstk provides a concise and effective method to achieve type safety as opposed to manual type checking.
- With
typeof, etc.const data = localStorage.getItem("config") if (data) { const config = JSON.parse(data) as unknown if ( config && typeof config === "object" && "theme" in config && typeof config.theme === "string" && ["light", "dark"].includes(config.theme) && "notifications" in config && typeof config.notifications === "boolean" ) { config /** ┌────────────────────────────────────┐ │ const config: object │ │ & Record<"theme", unknown> │ │ & Record<"notifications", unknown> │ └────────────────────────────────────┘ */ } } - With tstk
import { is } from "tstk" const data = localStorage.getItem("config") if (data) { const config = JSON.parse(data) if (is(config, { theme: union("light", "dark"), notifications: "boolean" })) { config /** ┌──────────────────────────────┐ │ const config: { │ │ theme: "light" | "dark"; │ │ notifications: boolean; │ │ } │ └──────────────────────────────┘ */ } }
Detailed Usage
Below is a more comprehensive reference showing how to check for primitives, classes, unions, arrays, records, tuples, and even complex schemas.
- Primitive type: "string"
if (is(value, "string")) { value /** ┌─────────────────────┐ │ const value: string │ └─────────────────────┘ */ } - Primitive type: "number"
if (is(value, "number")) { value /** ┌─────────────────────┐ │ const value: number │ └─────────────────────┘ */ } - Primitive type: "bigint"
if (is(value, "bigint")) { value /** ┌─────────────────────┐ │ const value: bigint │ └─────────────────────┘ */ } - Primitive type: "boolean"
if (is(value, "boolean")) { value /** ┌──────────────────────┐ │ const value: boolean │ └──────────────────────┘ */ } - Primitive type: "symbol"
if (is(value, "symbol")) { value /** ┌─────────────────────┐ │ const value: symbol │ └─────────────────────┘ */ } - Primitive type: "object"
if (is(value, "object")) { value /** ┌─────────────────────┐ │ const value: object │ └─────────────────────┘ */ }
[!NOTE] Unlike JavaScript's
typeofoperator,is(value, "object")includes functions (for whichtypeofreturns "function") and excludes null (an infamous ~~bug~~ feature oftypeof).is({}, "object") // true is([], "object") // true is(() => {}, "object") // true is(null, false) // false
- Primitive type: "record"
if (is(value, "record")) { value /** ┌─────────────────────────────────────────┐ │ const value: Record<keyof any, unknown> │ └─────────────────────────────────────────┘ */ }
[!TIP] Use the "record" primitive to match a plain object only.
is({}, "record") // true is([], "record") // false is(() => {}, "record") // false is(null, false) // false
- Primitive type: "array"
if (is(value, "array")) { value /** ┌─────────────────────────────────┐ │ const value: readonly unknown[] │ └─────────────────────────────────┘ */ } - Primitive type: "function"
if (is(value, "function")) { value /** ┌──────────────────────────────────────────────┐ │ const value: (...args: unknown[]) => unknown │ └──────────────────────────────────────────────┘ */ } - Primitive type: "any"
if (is(value, "any")) { value /** ┌──────────────────┐ │ const value: any │ └──────────────────┘ */ } - Primitive type: "null"
if (is(value, "null")) { value /** ┌───────────────────┐ │ const value: null │ └───────────────────┘ */ } - Primitive type: "undefined"
if (is(value, "undefined")) { value /** ┌────────────────────────┐ │ const value: undefined │ └────────────────────────┘ */ } - Literal type: string value
if (is(value, "hello")) { value /** ┌──────────────────────┐ │ const value: "hello" │ └──────────────────────┘ */ } - Literal type: number value
if (is(value, 42)) { value /** ┌─────────────────┐ │ const value: 42 │ └─────────────────┘ */ } - Literal type: bigint value
if (is(value, 21n)) { value /** ┌──────────────────┐ │ const value: 21n │ └──────────────────┘ */ } - Literal type: boolean value
if (is(value, true)) { value /** ┌───────────────────┐ │ const value: true │ └───────────────────┘ */ } - Literal type: symbol value
const $foo = Symbol("foo") if (is(value, symbol)) { value /** ┌──────────────────────────┐ │ const value: typeof $foo │ └──────────────────────────┘ */ } - Literal type: null value
if (is(value, null)) { value /** ┌───────────────────┐ │ const value: null │ └───────────────────┘ */ } - Literal type:
literalvalueif (is(value, literal("string"))) { value /** ┌───────────────────────┐ │ const value: "string" │ └───────────────────────┘ */ }
[!TIP] Use
literalto match a literal primitive type like "string" or "number".
- Class type
if (is(value, Date)) { value /** ┌───────────────────┐ │ const value: Date │ └───────────────────┘ */ } - Union type
if (is(value, union("string", "number"))) { value /** ┌──────────────────────────────┐ │ const value: string | number │ └──────────────────────────────┘ */ } - Joint type
if (is(value, joint({ foo: "string" }, { bar: "number" }))) { value /** ┌───────────────────────────────────────────┐ │ const value: { foo: string; bar: number } │ └───────────────────────────────────────────┘ */ } - Array type
if (is(value, array("string"))) { value /** ┌───────────────────────┐ │ const value: string[] │ └───────────────────────┘ */ } - Tuple type
if (is(value, ["string", "number"])) { value /** ┌───────────────────────────────┐ │ const value: [string, number] │ └───────────────────────────────┘ */ }
[!NOTE]
tuplecan also be used to define a tuple type.if (is(value, tuple("string", "number"))) { value /** ┌───────────────────────────────┐ │ const value: [string, number] │ └───────────────────────────────┘ */ }
- Record type: collective keys
if (is(value, record("string", "number"))) { value /** ┌─────────────────────────────────────┐ │ const value: Record<string, number> │ └─────────────────────────────────────┘ */ } - Record type: concrete keys
if (is(value, record(["foo", "bar"], "string"))) { value /** ┌────────────────────────────────────────────┐ │ const value: Record<"foo" | "bar", string> │ └────────────────────────────────────────────┘ */ } - Simple schema
if (is(value, { foo: "string" })) { value /** ┌──────────────────────────────┐ │ const value: { foo: string } │ └──────────────────────────────┘ */ }
[!NOTE] By default,
isdoes an exact match on the schema. To allow extra properties, passfalseas the third argument.is({ foo: 1, bar: 2 }, { foo: "number" }) // false is({ foo: 1, bar: 2 }, { foo: "number" }, false) // true
- Complex schema
if (is(value, Profile)) { value /** ┌───────────────────────────────────────────────┐ │ const value: { │ │ user: { │ │ userid: string; │ │ name: string; │ │ age: number; │ │ email: string; │ │ deleted: boolean; │ │ }; │ │ address: { │ │ street: string; │ │ city: string; │ │ zipcode: string; │ │ country: string; │ │ }; │ │ settings: { │ │ theme: "light" | "dark"; │ │ notifications: { │ │ email?: boolean | undefined; │ │ sms?: boolean | undefined; │ │ }; │ │ }; │ │ roles: ("admin" | "editor" | "viewer")[]; │ │ posts: { │ │ id: string; │ │ title: string; │ │ body: string; │ │ attachment?: string | undefined; │ │ publishedAt: number; │ │ tags: string[]; │ │ }[]; │ │ friends: { │ │ userid: string; │ │ name: string; │ │ startedAt: number; │ │ }[]; │ │ } │ └───────────────────────────────────────────────┘ */ }
const User = {
userid: primitive("string"),
name: primitive("string"),
age: primitive("number"),
email: primitive("string"),
deleted: primitive("boolean"),
}
const Address = record(["street", "city", "zipcode", "country"], "string")
const Settings = {
theme: union("light", "dark"),
notifications: partial(record(["email", "sms"], "boolean")),
}
const Role = union("admin", "editor", "viewer")
const Post = {
id: primitive("string"),
title: primitive("string"),
body: primitive("string"),
attachment: optional("string"),
publishedAt: primitive("number"),
tags: array("string"),
}
const Friend = joint(
pick(User, ["userid", "name"]),
{ startedAt: primitive("number") },
)
const Profile = {
user: User,
address: Address,
settings: Settings,
roles: array(Role),
posts: array(Post),
friends: array(Friend),
}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.
[!TIP] Combine
assertwithisorhasto narrow types at runtime effectively.assert(is(value, "string"), "Value must be a string") value /** ┌─────────────────────┐ │ const value: string │ └─────────────────────┘ */
Type Descriptors
primitive(type)
Define a primitive type such as "string" or "number".
[!TIP] Use
primitiveto define a primitive property in a schema.const Foo = { foo: primitive("number") } /** ┌──────────────────────────────┐ │ const Foo: { foo: "number" } │ └──────────────────────────────┘ */ if (is(value, Foo)) { value /** ┌──────────────────────────────┐ │ const value: { foo: number } │ └──────────────────────────────┘ */ }
literal(type)
Define a literal type such as literal("hello") or literal(42).
[!TIP] Use
literalto define a literal property in a schema and/or to match a literal primitive type.const Bar = { bar: literal("number") } /** ┌───────────────────────────────────────┐ │ const Bar: { bar: Literal<"number"> } │ └───────────────────────────────────────┘ */ if (is(value, Bar)) { value /** ┌────────────────────────────────┐ │ const value: { bar: "number" } │ └────────────────────────────────┘ */ }
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 where every element matches the corresponding type in types.
[!IMPORTANT] The length must be exactly the same as
types.
record(props, type)
Define a record type that matches a plain object with props, where all values match type.
[!NOTE] A collective record such as
record("string", "number")checks that every prop matchesprops.A concrete record such as
record(["foo", "bar"], "number")checks that allpropsare present.
partial(record)
Convert all properties of record to optional.
[!NOTE]
partialonly works with concrete records or schemas. To create a partial collective schema, wrap the value type inoptionalinstead.
optional(type)
Define an optional property that matches type.
readonly(type)
Define a readonly property that matches type.
Utility Functions
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 to object 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(target, ...sources)
Copy properties from each source into target, with last taking precedence.
Contributing
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.
License
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 🇸🇬.
