npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

stupid-types

v2.0.0

Published

Runtime type guards that narrow, so TypeScript stops fighting you.

Downloads

314

Readme

stupid-types

Do you love JavaScript? Yes, I love her. But she also has some disadvantages, which keeps her from being a perfect language. I never thought it was her "mistake". In fact, I think it is her obsessive persistence, for she went through a long, difficult time. Instead of staying away from her, we'd better resolve those problems for her.

你爱JS吗?是的,我爱。但是,她似乎又有一些令人尴尬的瑕疵,使她并不是理想中完美的存在。我从不认为这是她的“错误”,我知道,这是她在漫长历史中历遍磨难而养成的倔强。我们不应该因此将她敬而远之,而是为她抚平伤痕。

Feature / 功能

Runtime type guards that actually narrow. Zero runtime dependencies.

运行时类型判断,真的会收窄类型。零运行时依赖。

Every guard takes unknown and returns a type predicate, so after the check TypeScript knows what you have — not just that some condition held.

每个判断的入参都是 unknown,返回的都是类型谓词。所以判断通过之后,TypeScript 是真的 知道你手里是什么类型,而不只是「某个条件成立了」。

The design intent is that a guard answers a question rather than raising one: it returns a boolean instead of throwing.

设计上,守卫的职责是回答问题,而不是抛出新问题:给任何输入都返回布尔值,而不是抛错。


Why / 为什么

Checking "simple" types in JS is disproportionately annoying. The naive versions are all subtly wrong:

在 JS 里判断「简单」类型,麻烦得不成比例。那些想当然的写法几乎全都有坑:

| You want / 你想判断 | Naive / 想当然的写法 | What breaks / 哪里坏了 | |---|---|---| | a number | typeof x === 'number' | NaN and Infinity pass | | a plain object | typeof x === 'object' | null and arrays pass too | | a plain object | Object.prototype.toString | class instances report [object Object] | | not empty | if (x) | 0, '', false, NaN all fail | | an array | Array.isArray(x) | narrows to any[], poisoning everything downstream | | a Map | x instanceof Map | fails for any Map from another realm | | a Date | x instanceof Date | fails across realms, and new Date('x') is a Date but unusable | | an Error | x instanceof Error | fails across realms |

This package is the version where those are already handled.

这个包就是「这些都已经处理好了」的那一版。

Install / 安装

npm install stupid-types

Versions. 1.0.0 was an early release and should not be used. 2.0.0 supersedes it and contains breaking changes against it.

版本。 1.0.0 是早期发布,请勿使用。2.0.0 取代了它,相对它含有破坏性变更。

Requires an ES2020+ runtime (the package ships a isBigInt guard).

需要 ES2020+ 运行时(包里有 isBigInt)。

Ships ESM + CJS + type declarations. 同时提供 ESM、CJS 和类型声明。

Usage / 用法

import { isPlainObject, isNonEmptyArray, isValidDate } from 'stupid-types';

// Narrowing actually happens / 真的会收窄
const input: unknown = JSON.parse(payload);
if (isPlainObject(input)) {
  // input is `object` here — see the note below on why not a Record
  // 这里 input 是 `object`——为什么不是 Record 见下面
}

// A Date must be usable, not merely a Date / 得是「能用」的日期,而不只是一个 Date
const when: unknown = row.createdAt;
if (isValidDate(when)) {
  when.getTime(); // Date — and toISOString() will not throw a RangeError
}

// An array check that does not leak `any` / 不会泄漏 any 的数组判断
declare const rows: unknown;
if (isNonEmptyArray(rows)) {
  const count: number = rows.length; // `rows` is `unknown[]`, not `any[]`
  // `rows[0]` is still `unknown` — non-emptiness does not make it into the type
  // `rows[0]` 仍然是 `unknown`——「非空」这件事没有进到类型里
}

API

Full bilingual docs (including the easy-to-get-wrong points) are in the emitted .d.ts, so they show up in your editor on hover.

完整的双语文档(含易错点)都写在生成的 .d.ts 里,编辑器悬停即可看到。

Type identification / 类型识别

| Guard | Narrows to | Notes | |---|---|---| | isUndefined(x) | undefined | | | isNull(x) | null | typeof null is 'object', so only === null works | | isBoolean(x) | boolean | new Boolean(false) is an object — and truthy | | isNumber(x) | number | mirrors typeof: NaN and Infinity count | | isString(x) | string | new String('a') is an object, not a string | | isBigInt(x) | bigint | | | isSymbol(x) | symbol | | | isFunction(x) | Function | classes, async, and generators all count | | isArray(x) | unknown[] | not any[], unlike Array.isArray — see Caveats | | isMap(x) | Map<unknown, unknown> | cross-realm safe, spoofable | | isSet(x) | Set<unknown> | cross-realm safe, spoofable | | isDate(x) | Date | an unparseable date still passes — see isValidDate | | isRegExp(x) | RegExp | cross-realm safe, spoofable | | isError(x) | Error | all subclasses; DOMException does not pass | | isPlainObject(x) | object | rejects class instances, arrays, built-ins |

Numbers / 数字

| Guard | Notes | |---|---| | isValidNumber(x) | not NaN — the infinities still pass | | isFiniteNumber(x) | not NaN and not an infinity; unlike the global isFinite, never coerces | | isInteger(x) | 42.0 passes; every non-number fails, without coercion |

Nil / 空值

| Guard | Notes | |---|---| | isNullOrUndefined(x) | null and undefined only — '', 0, NaN are values, not nil |

Arrays / 数组

| Guard | Notes | |---|---| | isNonEmptyArray(x) | narrows to unknown[], same as isArray — non-emptiness is not in the type |

Strings / 字符串

| Guard | Notes | |---|---| | isNonEmptyString(x) | whitespace-only strings count as non-empty — no trimming |

Dates / 日期

| Guard | Notes | |---|---| | isValidDate(x) | a date that is actually usable; new Date('garbage') fails |

Caveats / 已知边界

Documented on purpose, not oversights. 这些是刻意写明的,不是疏漏。

Cross-realm vs. spoofable. isMap, isSet, isDate, isRegExp, and isError use Object.prototype.toString. Here that is reliable, because each of these types carries a tag of its own — unlike a plain object, whose [object Object] is shared with class instances (which is why isPlainObject walks the prototype chain instead). It makes them correct across realms (iframe, worker, node:vm) where instanceof silently fails — at the cost of being fooled by an object carrying a fake Symbol.toStringTag:

跨 realm 正确 vs. 可被伪造。 这几个走 Object.prototype.toString。这里它是可靠的,因为这些类型 各自有专属标签——而纯对象的 [object Object] 是和类实例共用的(所以 isPlainObject 改查原型链)。 因此它们在 instanceof 会静默失效的跨 realm 场景(iframe、worker、node:vm)里仍然正确, 代价是能被伪造的 Symbol.toStringTag 骗过:

isMap({ [Symbol.toStringTag]: 'Map' }); // true — spoofed

isArray is the exception: it uses Array.isArray, which is both cross-realm safe and not spoofable.

isArray 是例外:它走 Array.isArray,既跨 realm 正确,也不可伪造。

isValidDate is the other exception: instead of asking "what does this claim to be", it asks the value for its time, which a fake cannot supply. So a fake tag gets past isDate but not past isValidDate — and it still works across realms.

isValidDate 是另一个例外:它不问「这个东西自称是什么」,而是直接向它要时间值——伪造的东西 给不出来。所以伪造的标签能骗过 isDate,骗不过 isValidDate。跨 realm 也依然可用。

isPlainObject narrows to object, not Record<string, unknown>. A Record predicate would assert a string index signature that a plain object does not actually have. The cost is that you cannot index straight after narrowing:

isPlainObject 收窄成 object,不是 Record<string, unknown>。 用 Record 会断言一个纯对象并不实际拥有的字符串索引签名。代价是收窄后不能直接索引:

if (isPlainObject(v)) v.anyKey; // error: `object` has no index signature

isNumber mirrors typeof. isNumber(NaN) is true. Use isValidNumber or isFiniteNumber when you want a usable number.

isNumber 照 typeof 直译。 isNumber(NaN) 是 true。要能用的数请用 isValidNumber 或 isFiniteNumber。

isArray and isNonEmptyArray do not preserve readonly. Both narrow to unknown[], so a readonly number[] comes back writable. Since Object.freeze is typed as returning readonly, this compiles and then throws:

isArray 和 isNonEmptyArray 不保留 readonly。 两者都收窄成 unknown[],所以 readonly number[] 收窄后变成可写。Object.freeze 的返回类型正是 readonly,于是下面这段 编译通过、运行时抛错:

const frozen = Object.freeze([1, 2, 3]); // readonly number[]
if (isArray(frozen)) frozen.push(4);     // TypeError: object is not extensible

Array.isArray's predicate is any[] and behaves the same way. Narrowing to readonly unknown[] instead would avoid this, at the cost of making values narrowed from unknown read-only too. If you need to write to the array, copy it first.

Array.isArray 的谓词是 any[],行为相同。改成收窄 readonly unknown[] 可以避开这一点, 代价是从 unknown 收窄进来的值也变成只读。要写就先拷一份。

isNonEmptyArray puts nothing extra in the type, and trusts .length. Non-emptiness cannot be expressed without a tuple, and a tuple would cost you the element type — number[] would narrow to something whose [0] reads back as unknown while push stayed legal. So it narrows to unknown[], the same as isArray: check the length yourself. And a container that lies about its length cannot be told apart from an honest one — a hard limit, not a trade.

isNonEmptyArray 不往类型里放额外信息,而且信任 .length。 「非空」不用元组就没法进类型, 而元组会让你丢掉元素类型——number[] 收窄后 [0] 读出来是 unknown,而 push 依然合法。 所以它和 isArray 一样收窄成 unknown[],长度请自己查。至于谎报 length 的容器,无法与诚实的 容器区分——那是硬限制,不是取舍。

Not included, deliberately.

  • isObject — typeof, TypeScript's object, and intuition all disagree about whether functions count. Any name here would mislead someone.
  • isEmail, isUrl and friends — that is format validation, which is a different job.
  • Anything generic — helpers that take a type parameter or a callback to validate elements against. This package is JavaScript-first: it answers "what is this value", it does not do generic validation.

刻意不提供的东西。

  • isObject —— typeof、TS 的 object 类型和人的直觉对「函数算不算」三方不一致, 提供出来必然误导一部分人。
  • isEmail / isUrl 之类 —— 那是格式校验,另一回事。
  • 一切泛型相关的东西 —— 带类型参数、或接收回调去校验元素的辅助函数。这个包是 JavaScript 优先的:它只回答「这个值是什么」,不做泛型校验。

Development / 开发

npm run build      # tsup → dist/ (ESM + CJS + .d.ts)
npm test           # vitest
npm run typecheck  # tsc --noEmit

The toolchain needs a newer Node than the package does. The published package runs on Node 14+ — that is what engines in package.json declares, and it is the only Node requirement a consumer inherits. The dev floor is set by the test runner's own engines, not by this package, so check there first when something fails to install or run. The two numbers move independently.

工具链需要的 Node 版本比这个包本身高。发布出去的包在 Node 14+ 上就能跑——那是 package.json 里 engines 声明的,也是使用者唯一需要继承的要求。开发的底线由测试运行器自己的 engines 决定,不是这个包定的;装不上或跑不起来时先去那里看。两个数字各自变化。

AI Disclosure / AI 使用声明

Parts of this project were developed with AI assistance. We disclose the AI agents/tools used and, where known, the underlying models. AI-assisted output is reviewed as necessary, and tested by maintainers, who take responsibility for the final result. Contributors using AI agents or models must disclose them in the PR description.

本项目部分代码/文档使用 AI 工具辅助生成,维护者进行必要的人工审查,以及测试和修改。项目维护者对最终内容负责。贡献者如使用 AI,请在 PR 中披露。

  • Tools / 工具:Claude Code
  • Underlying model / 底层模型:DeepSeek-v4.1-flash

License / 许可

MIT