Trick or Type Guard? Surviving `unknown` with is-kit π
Hoi hoi! π I'm @nyaomaru, a frontend engineer who is scared of horror things π Happy Halloween! ππ» Tonight, we're exploring a haunted TypeScript project. There are zombies π§, ghosts π», mysterious pumpkins π, and a te
Hoi hoi! π
I'm @nyaomaru, a frontend engineer who is scared of horror things π
Happy Halloween! ππ»
Tonight, we're exploring a haunted TypeScript project.
There are zombies π§, ghosts π», mysterious pumpkins π, and a terrifying vampire π§ waiting for us.
And they all have something to do with TypeScript types!
Our mission?
Survive the night without losing type safety! πΈ
Let's go!
π§ Chapter 1: The Unknown Living Dead
It's a dark and spooky night.
Suddenly, something crawls out of the grave! π§
const creature: unknown = JSON.parse('{"message":"BRAAAINS!"}');
Oh! An unknown creature!
Let's see what it wants.
console.log(creature.message);
// β 'creature' is of type 'unknown'.
WHAT?! π
TypeScript won't let us!
But why?
Well, because we don't actually know what creature is.
It might be a zombie.
It might be a ghost.
It might even be a cute little cat. πΈ
unknown means we haven't verified the value yet.
Unlike any, TypeScript won't let us access arbitrary properties on an unknown value.
And that's actually a good thing!
Let's try something simpler.
import { isString } from "is-kit";
const voice: unknown = JSON.parse('"BRAAAINS!"');
if (isString(voice)) {
console.log(voice.toUpperCase());
// BRAAAINS!
}
YAY! π
We survived our first encounter!
Of course, we could also use typeof voice === 'string' here.
For a single check, that's perfectly fine!
But what happens when our monsters become bigger and more complicated?
π Chapter 2: The Big Pumpkin of Unknown
Look!
A GIGANTIC PUMPKIN! π
And apparently, it's full of candy.
But nobody knows what's really inside.
const pumpkin: unknown = JSON.parse(`{
"name": "Big Pumpkin",
"candies": ["Chocolate", "Lollipop", "Cookie"],
"spookyLevel": 99,
"keeper": {
"name": "cat",
"friendly": true
},
"secret": "Meow!"
}`);
That's a pretty big object.
Can we just grab the candy?
console.log(pumpkin.candies);
// β 'pumpkin' is of type 'unknown'.
Not again! πΏ
We need to check what's inside before touching anything.
Fortunately, we have struct!
import {
arrayOf,
isBoolean,
isNumber,
isString,
optionalKey,
struct,
} from "is-kit";
const isPumpkin = struct({
name: isString,
candies: arrayOf(isString),
spookyLevel: isNumber,
keeper: struct({
name: isString,
friendly: isBoolean,
}),
secret: optionalKey(isString),
});
We composed several small guards into one big pumpkin guard.
-
isStringchecks strings. -
isNumberchecks finite numbers. -
arrayOf(isString)checks an array of strings. -
structchecks nested object shapes. -
optionalKeyallows a property to be missing.
Now, let's open the pumpkin!
import { safeParse } from "is-kit";
const result = safeParse(isPumpkin, pumpkin);
if (result.valid) {
console.log(result.value.candies);
// ['Chocolate', 'Lollipop', 'Cookie']
console.log(result.value.keeper.name);
// cat πΈ
}
We found the candy!!! π¬π
When validation succeeds, result.value has the type inferred from our guards.
And when validation fails?
const cursedPumpkin = {
name: "Cursed Pumpkin",
candies: "100 candies",
spookyLevel: 99,
keeper: {
name: "cat",
friendly: true,
},
};
const result = safeParse(isPumpkin, cursedPumpkin);
console.log(result);
// { valid: false }
The candies were a string, not an array!
Our guard rejected the cursed pumpkin.
And here's the important part.
safeParse doesn't magically fix or convert invalid values.
It simply tells us whether the value passed our guard.
Also, struct allows extra properties by default. If you need to reject additional own enumerable string keys, you can use { exact: true }.
Nice and simple!
π¬ Chapter 3: Trick or Type Guard!
Ding dong! π
TRICK OR TREAT!!! πΈ
Someone hands us a mysterious gift.
function receiveGift(): unknown {
return Math.random() > 0.5 ? "Chocolate" : { spiders: 100 };
}
const gift: unknown = receiveGift();
Wait.
Is it candy?
Or is it a box full of spiders?! π·οΈπ±
We need a way to check!
And this time, we want to accept either a string or a number.
Let's combine our guards with or!
import { isNumber, isString, or } from "is-kit";
const isTreat = or(isString, isNumber);
function trickOrTreat(gift: unknown) {
if (isTreat(gift)) {
// gift: string | number
return `π¬ Treat: ${gift}`;
}
return "π» Trick!";
}
Now let's try it!
trickOrTreat("Chocolate");
// π¬ Treat: Chocolate
trickOrTreat(100);
// π¬ Treat: 100
trickOrTreat({ spiders: 100 });
// π» Trick!
YAY!!! π¬
No spiders for us today!
This is one of my favorite things about is-kit.
We don't need to write one giant validator.
We can build small guards, combine them, and reuse them wherever we need.
Small guards. Simple composition. Useful TypeScript narrowing.
That's the idea!
But wait,
Something is coming.
Something TERRIFYING.
π§ Final Boss: The Vampire Called any
Suddenly, the lights go out.
The door opens.
A mysterious figure appears in the darkness.
It's... COUNT ANY!!! π§π
The most terrifying vampire in our TypeScript project! And he has a very dangerous power.
He can make TypeScript stop checking our operations!
const visitor: unknown = JSON.parse('{"name":666}');
A mysterious visitor has arrived.
Normally, TypeScript protects us from accessing properties before we verify them.
But Count Any has a trick.
THE CURSE OF as any! π§
const vampire = visitor as any;
// The vampire ignores type safety!
console.log(vampire.name.toUpperCase());
No TypeScript error!
Wait, What?! π
Does that mean the code is safe?
At runtime
TypeError: vampire.name.toUpperCase is not a function
π GAME OVER π
Count Any didn't turn the number into a string.
He just convinced TypeScript to stop checking it!
That's what makes any dangerous.
It allows operations without requiring proof that they'll work.
But unknown behaves differently.
const visitor: unknown = JSON.parse('{"name":666}');
console.log(visitor.name.toUpperCase());
// β TypeScript stops us!
TypeScript refuses to continue until we check the value.
So how do we defeat Count Any?
With a Type Guard, of course! πΈ
import { isString, struct } from "is-kit";
const isFriendlyGuest = struct({
name: isString,
});
if (isFriendlyGuest(visitor)) {
console.log(visitor.name.toUpperCase());
} else {
console.log("π§ Not tonight, Count Any!");
}
And just like that
THE VAMPIRE HAS BEEN DEFEATED! π§π
Well, at least this particular cursed value has been rejected.
A Type Guard checks the actual runtime value, while a type assertion only changes what TypeScript believes about it.
One important detail, neither unknown nor any changes the value at runtime.
They are TypeScript types.
The difference is what the compiler allows us to do before verifying the value.
And that's why unknown is usually the safer choice for untrusted data.
πΈ Epilogue: We Survived Halloween!
What a night!
We met the Unknown Living Dead, opened a gigantic pumpkin, collected some candy, and defeated Count Any!
So, what did we learn?
| Monster | TypeScript lesson |
|---|---|
| π§ Unknown Living Dead |
unknown prevents unchecked operations |
| π Big Pumpkin |
struct validates object shapes |
| π¬ Trick or Treat |
or combines reusable type guards |
| π§ Count Any |
any bypasses normal type checking |
Of course, any isn't always evil.
Sometimes it's useful when dealing with legacy code or difficult integrations.
But when working with API responses, parsed JSON, and other untrusted values, keeping them as unknown until validation is usually a much safer approach.
And is-kit makes those checks easy to compose and reuse!
If you want to explore more, check out the official guide π
π How to Validate unknown in TypeScript Without a Schema Library
And remember
The real monster isn't unknown. It's unchecked any. π§
Happy Halloween!!! ππ»
If you enjoyed this little TypeScript adventure, check out is-kit!
nyaomaru
/
is-kit
Build small guards. Compose them. A lightweight, zero-dependency toolkit for building reusable TypeScript type guards that compose, refine, and preserve natural narrowing. Runtime-safe π‘οΈ, composable π§©, and ergonomic β¨.
is-kit
Build small guards. Compose them.
is-kit is a lightweight, zero-dependency toolkit for building reusable TypeScript type guards.
is-kit is not just a collection of isX helpers. Its main focus is composing
small runtime checks into reusable guards while preserving useful TypeScript
narrowing.
It helps you write small isFoo functions, compose them into richer runtime
checks, refine properties on values you already know about, and keep
TypeScript narrowing natural inside regular control flow. Use it at runtime
boundaries when needed, without requiring a schema-first workflow.
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 - Use
refineKeyto narrow a child property while preserving its parent type - Validate object shapes and collections when that is useful
-
Parse or assert
unknownvalues without a large schema framework
Bestβ¦
And if you like the project, a β would make this little cat very happy! πΈ
Thank you for reading! π€²
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.
