Hoi hoi! 👋
我是 @nyaomaru,一位很怕恐怖事物的前端工程師 🙀
萬聖節快樂! 🎃👻
今晚,我們要探索一個鬧鬼的 TypeScript 專案。
這裡有殭屍 🧟、鬼魂 👻、神祕南瓜 🎃,還有一隻可怕的吸血鬼 🧛 等著我們。
而且它們都和 TypeScript 型別有關!
我們的任務是?
在不失去型別安全的情況下撐過這一晚! 😸
出發吧!

這是一個黑暗又陰森的夜晚。
突然,有東西從墳墓裡爬出來了! 🧟
const creature: unknown = JSON.parse('{"message":"BRAAAINS!"}');
喔!是一個 unknown 生物!
來看看它想幹嘛。
console.log(creature.message);
// ❌ 'creature' 的型別是 'unknown'。
什麼?!🙀
TypeScript 不讓我們這樣做!
但是為什麼?
因為我們其實不知道 creature 到底是什麼。
它可能是殭屍。
它可能是鬼魂。
它甚至可能是一隻可愛的小貓。😸
unknown 表示我們還沒有驗證這個值。
跟 any 不同,TypeScript 不會讓我們對 unknown 值任意存取屬性。
而這其實是好事!
我們來試個更簡單的例子。
import { isString } from "is-kit";
const voice: unknown = JSON.parse('"BRAAAINS!"');
if (isString(voice)) {
console.log(voice.toUpperCase());
// BRAAAINS!
}
YA!🎉
我們撐過第一次遭遇了!
當然,這裡我們也可以直接用 typeof voice === 'string'。
如果只做單一檢查,這完全沒問題!
但當怪物變得更大、更複雜時,又會怎樣呢?
看!
一顆超巨大的南瓜! 🎃
而且看起來裡面裝滿了糖果。
但沒有人知道裡面到底有什麼。
const pumpkin: unknown = JSON.parse(`{
"name": "Big Pumpkin",
"candies": ["Chocolate", "Lollipop", "Cookie"],
"spookyLevel": 99,
"keeper": {
"name": "cat",
"friendly": true
},
"secret": "Meow!"
}`);
這是一個相當大的物件。
我們可以直接拿糖果嗎?
console.log(pumpkin.candies);
// ❌ 'pumpkin' 的型別是 'unknown'。
又來了!😿
我們必須先檢查裡面有什麼,才能碰它。
幸好,我們有 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),
});
我們把好幾個小型 guard 組合成一個大型南瓜 guard。
isString 檢查字串。isNumber 檢查有限數字。arrayOf(isString) 檢查字串陣列。struct 檢查巢狀物件結構。optionalKey 允許某個屬性不存在。現在,來打開南瓜吧!
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 😸
}
我們找到糖果啦!!!🍬🎉
當驗證成功時,result.value 會根據我們的 guards 推斷出型別。
那如果驗證失敗呢?
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 }
糖果是字串,不是陣列!
我們的 guard 擋下了被詛咒的南瓜。
而這裡很重要的一點是:
safeParse 不會神奇地修正或轉換不合法的值。
它只是告訴我們這個值有沒有通過 guard。
另外,struct 預設允許額外屬性存在。如果你需要拒絕額外的自有可列舉字串鍵,可以使用 { exact: true }。
簡單又好用!
叮咚!🔔
不給糖就搗蛋!!!😸
有人遞給我們一份神祕禮物。
function receiveGift(): unknown {
return Math.random() > 0.5 ? "Chocolate" : { spiders: 100 };
}
const gift: unknown = receiveGift();
等等。
這是糖果嗎?
還是一盒蜘蛛?!🕷️😱
我們需要一種方法來檢查它!
而這次,我們想接受字串或數字其中之一。
讓我們用 or 把 guards 組合起來!
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!";
}
現在來試試看!
trickOrTreat("Chocolate");
// 🍬 Treat: Chocolate
trickOrTreat(100);
// 🍬 Treat: 100
trickOrTreat({ spiders: 100 });
// 👻 Trick!
YA!!!🍬
今天不用面對蜘蛛了!
這正是我很喜歡 is-kit 的地方之一。
我們不需要寫一個巨大的驗證器。
我們可以建立小型 guards,把它們組合起來,然後在需要的地方重複使用。
小型 guards、簡單組合、實用的 TypeScript 縮窄。
概念就是這樣!
但是等等,
有什麼東西正在靠近。
而且是非常可怕的東西。
any 的吸血鬼突然,燈熄了。
門開了。
黑暗中出現了一個神祕身影。
那是……ANY 伯爵!!!🧛💀
我們 TypeScript 專案裡最可怕的吸血鬼!而且他有一個非常危險的能力。
他能讓 TypeScript 停止檢查我們的操作!
const visitor: unknown = JSON.parse('{"name":666}');
一位神祕訪客來了。
通常,TypeScript 會在我們驗證之前保護我們,避免直接存取屬性。
但 Any 伯爵有一招。
as any 的詛咒!🧛
const vampire = visitor as any;
// 這個吸血鬼會無視型別安全!
console.log(vampire.name.toUpperCase());
沒有 TypeScript 錯誤!
等等,什麼?!🙀
那是不是代表這段程式碼是安全的?
在執行時
TypeError: vampire.name.toUpperCase is not a function
💀 遊戲結束 💀
Any 伯爵並沒有把數字變成字串。
他只是讓 TypeScript 不要再檢查它了!
這就是 any 危險的地方。
它允許我們在沒有證明操作可行的情況下直接執行。
但 unknown 的行為不同。
const visitor: unknown = JSON.parse('{"name":666}');
console.log(visitor.name.toUpperCase());
// ❌ TypeScript 擋下我們!
TypeScript 會拒絕繼續,直到我們檢查這個值為止。
那我們要怎麼打敗 Any 伯爵?
當然是靠 Type Guard!😸
import { isString, struct } from "is-kit";
const isFriendlyGuest = struct({
name: isString,
});
if (isFriendlyGuest(visitor)) {
console.log(visitor.name.toUpperCase());
} else {
console.log("🧄 今天不行,Any 伯爵!");
}
就這樣
吸血鬼被擊敗了! 🧄🎉
好吧,至少這個被詛咒的值已經被拒絕了。
Type Guard 會檢查實際的執行時值,而型別斷言只是在改變 TypeScript 對它的認知。
有一個重要細節,unknown 和 any 都不會改變執行時的值。
它們都是 TypeScript 的型別。
差別在於,在驗證值之前,編譯器允許我們做什麼。
所以,對於不可信任的資料,unknown 通常是更安全的選擇。
這真是漫長的一夜!
我們遇到了未知的活死人、打開了巨大的南瓜、收集了一些糖果,還打敗了 Any 伯爵!
所以,我們學到了什麼?
| 怪物 | TypeScript 課題 |
|---|---|
| 🧟 未知的活死人 | unknown 會阻止未經檢查的操作 |
| 🎃 巨大南瓜 | struct 用來驗證物件結構 |
| 🍬 不給糖就搗蛋 | or 可以組合可重用的型別守衛 |
| 🧛 Any 伯爵 | any 會繞過一般的型別檢查 |
當然,any 不一定總是邪惡的。
有時候,當你面對舊有程式碼或難以整合的系統時,它會很有用。
但在處理 API 回應、JSON 解析結果,以及其他不可信任的值時,在驗證之前先保持它們為 unknown,通常是更安全的做法。
而 is-kit 讓這些檢查更容易組合與重複使用!
如果你想了解更多,請看看官方指南 👇
📖 如何在不使用 Schema 函式庫的情況下於 TypeScript 中驗證 unknown
還有,記得
真正的怪物不是 unknown,而是未經檢查的 any。 🧛
萬聖節快樂!!!🎃👻
如果你喜歡這段小小的 TypeScript 冒險,來看看 is-kit 吧!
https://github.com/nyaomaru/is-kit
如果你喜歡這個專案,幫它點個 ⭐ 會讓這隻小貓非常開心!😸
謝謝閱讀!🤲
原文出處:https://dev.to/nyaomaru/trick-or-type-guard-surviving-unknown-with-is-kit-4oc2