DEV Community

Cover image for Handling `unknown` in TypeScript… isn't it painful?
nyaomaru
nyaomaru

Posted on

Handling `unknown` in TypeScript… isn't it painful?

Hi there 👋

I'm a frontend engineer based in the Netherlands, currently suffering from hay fever 😿

API responses, form inputs, external data…

In TypeScript, we often end up dealing with unknown,
and handling it properly can become a real pain in day-to-day work.

Yes, unknown has a kind of gravitational pull like the universe.

But still…
we do want to handle types safely, right?

That’s where is-kit comes in a library for composing type guards.

GitHub logo nyaomaru / is-kit

Lightweight, zero-dependency toolkit for building `isFoo` style type guards in TypeScript. Runtime-safe 🛡️, composable 🧩, and ergonomic ✨. npm -> https://www.npmjs.com/package/is-kit

is-kit

is-kit logo

npm version JSR npm downloads License

is-kit is a lightweight, zero-dependency toolkit for building reusable TypeScript type guards.

It helps you write small isFoo functions, compose them into richer runtime checks, and keep TypeScript narrowing natural inside regular control flow.

Runtime-safe 🛡️, composable 🧩, and ergonomic ✨ without asking you to adopt a heavy schema workflow.

  • Build and reuse typed guards
  • Compose guards with and, or, not, oneOf
  • Validate object shapes and collections
  • Parse or assert unknown values without a large schema framework

📚 Documentation Site

Best for app-internal narrowing, filtering, and reusable guards.

🤔 Why use is-kit?

Tired of rewriting the same isFoo checks again and again?

is-kit is a good fit when you want to:

  • write reusable isX functions instead of one-off inline checks
  • keep runtime validation lightweight and dependency-free
  • narrow values directly in if, filter, and other TypeScript control flow
  • compose validation logic

Libraries like Zod take a schema-first approach.

In contrast, is-kit focuses on narrowing types within your existing code.

Instead of “writing validation”, you can think of it as extending your everyday if statements with type safety.

If Zod is for boundaries (API / input)
is-kit is for inside your application logic


A simple example

Imagine you need to check “strings with length ≤ 3” multiple times.

With is-kit, you can define it once and reuse it:

import { define, isString } from "is-kit";

const isShortString = define<string>(
  (value) => isString(value) && value.length <= 3,
);
Enter fullscreen mode Exit fullscreen mode

Then use it like this:

import { isShortString } from "~/utils/is";

declare const input: unknown;

// before → repeating conditions every time
if (typeof input === "string" && input.length <= 3) {
  input.toUpperCase();
}

// after → reusable guard
if (isShortString(input)) {
  input.toUpperCase();
}
Enter fullscreen mode Exit fullscreen mode

This style works seamlessly with
if, filter, map, and other existing logic.


🐾 How is-kit has evolved

It’s been about 6 months since the v1.0 release.

Back then, is-kit started as a lightweight type guard library
with primitives like:

  • define
  • and
  • or
  • struct
  • arrayOf

Since then, up to v1.6, it has gradually evolved into something more practical:

👉 a toolkit for handling unknown values in real-world applications

Let’s look at 5 major improvements.


🪄 1. Distinguishing “missing” vs “undefined” in struct

A common situation in API responses:

  • A key is missing entirely
  • A key exists but its value is undefined

These are not the same.

With v1.5.0, optionalKey(...) was introduced:

import { isString, optional, optionalKey, struct } from "is-kit";

const isUser = struct({
  id: isString,
  nickname: optionalKey(isString),
  displayName: optionalKey(optional(isString)),
});
Enter fullscreen mode Exit fullscreen mode

This allows you to express both cases explicitly.


🔑 2. Key-based narrowing (hasKey, hasKeys, narrowKeyTo)

From v1.1.13 and v1.4.0, key-based utilities were added:

import {
  hasKeys,
  narrowKeyTo,
  oneOfValues,
  struct,
  isString,
  isNumber,
} from "is-kit";

const isUser = struct({
  id: isString,
  age: isNumber,
  role: oneOfValues("admin", "guest", "trial"),
});

const hasRoleAndId = hasKeys("role", "id");
const byRole = narrowKeyTo(isUser, "role");
const isGuest = byRole("guest");
Enter fullscreen mode Exit fullscreen mode

This lets you build on top of existing guards instead of redefining everything.


🧪 3. assert for fail-fast validation

Added in v1.2.0:

import { assert, isString } from "is-kit";

declare const input: unknown;

assert(isString, input, "input must be a string");
input.toUpperCase();
Enter fullscreen mode Exit fullscreen mode

You can now use guards in a fail-fast style when needed.


✨ 4. Support for Set and Map

With v1.6.0:

import { mapOf, setOf, isString, isNumber } from "is-kit";

const isTags = setOf(isString);
const isScores = mapOf(isString, isNumber);
Enter fullscreen mode Exit fullscreen mode

Not everything is an array in real-world apps —
this expands coverage to common JS data structures.


🥏 5. Handling numeric edge cases

From v1.1.x:

  • isInteger
  • isSafeInteger
  • isPositive
  • isNegative
  • isNaN
  • isInfiniteNumber
  • isZero

These help you express “valid numbers” more precisely.


🌟 More features

There are more utilities available —
feel free to explore the docs:


🎯 Summary

is-kit started as a small, composable type guard library.

Over time, it has evolved into:

👉 a practical toolkit for handling unknown in application code

Key improvements:

  • More expressive object schemas (optionalKey)
  • Better key-based narrowing
  • Fail-fast assertions
  • Collection support (Set, Map)
  • Rich numeric guards

The goal of is-kit is simple:

👉 Make type-safe code feel natural to write

If you try it and have ideas or feedback, feel free to share!

See you in the next post 👋

https://github.com/nyaomaru/is-kit

Top comments (0)