typanic 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kaspernj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # typanic
2
+
3
+ > type + panic — get the type you expect, or it throws.
4
+
5
+ Tiny, dependency-free runtime type assertions for **untrusted input** (request
6
+ and response bodies, webhook payloads, message/command payloads, parsed config,
7
+ route/query params). Instead of silently coercing a wrong value into `""`, `0`,
8
+ or `false` — which produces noise and hides a real producer bug far from where it
9
+ happens — `typanic` either gives you the value as the type you asked for, or
10
+ throws a `TypeError`.
11
+
12
+ ```js
13
+ // ❌ silent, noisy, swallows malformed data
14
+ const name = typeof payload.name === "string" ? payload.name : ""
15
+
16
+ // ✅ loud and clean
17
+ import {forcedString} from "typanic"
18
+ const name = forcedString(payload.name, "name") // throws if it isn't a string
19
+ ```
20
+
21
+ For values that are already typed/validated (a generated model accessor, a
22
+ boundary-typed param) you don't need this at all — just use them directly.
23
+ `typanic` is for the boundary where data is genuinely untrusted.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install typanic
29
+ ```
30
+
31
+ ESM only. Ships with TypeScript declarations (`.d.ts`) generated from JSDoc.
32
+
33
+ ## API
34
+
35
+ Every helper takes the value and an optional `label` used in the error message
36
+ (`Expected <label> to be a <type> but got <actual>`).
37
+
38
+ ### Required — throw when the value is the wrong type
39
+
40
+ | Function | Returns | Notes |
41
+ | --- | --- | --- |
42
+ | `forcedString(value, label?)` | `string` | throws unless `typeof value === "string"` |
43
+ | `forcedInteger(value, label?)` | `number` | accepts integers and integer-looking strings (`"42"`) |
44
+ | `forcedFloat(value, label?)` | `number` | accepts finite numbers and numeric strings; rejects `NaN`/`Infinity` |
45
+ | `forcedBoolean(value, label?)` | `boolean` | does **not** coerce `"true"`/`1` — pass a real boolean |
46
+
47
+ ### Optional — `null` when absent, throw when present-but-wrong-typed
48
+
49
+ | Function | Returns |
50
+ | --- | --- |
51
+ | `optionalString(value, label?)` | `string \| null` |
52
+ | `optionalInteger(value, label?)` | `number \| null` |
53
+ | `optionalFloat(value, label?)` | `number \| null` |
54
+ | `optionalBoolean(value, label?)` | `boolean \| null` |
55
+
56
+ `null` and `undefined` both count as "absent" and return `null`. A value that is
57
+ *present* but of the wrong type still throws — absence and corruption are
58
+ different things.
59
+
60
+ ```js
61
+ import {forcedInteger, optionalString} from "typanic"
62
+
63
+ const cols = forcedInteger(payload.cols, "cols") // number, or throws
64
+ const cursor = optionalString(payload.cursor, "cursor") // string | null
65
+ const status = optionalString(payload.status, "status") ?? "ok" // default only when you truly need one
66
+ ```
67
+
68
+ ## Why "forced"?
69
+
70
+ The value is *forced* to be the type you declared. There is no quiet fallback:
71
+ the data is what you expect, or your code stops at the boundary with a clear
72
+ error instead of carrying a silent empty string into the rest of the system.
73
+
74
+ ## License
75
+
76
+ MIT
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Returns the value when it is a string, otherwise throws. Use this for required
3
+ * values coming from untrusted input (request bodies, webhook payloads, message
4
+ * payloads, parsed config) instead of silently coercing a wrong type to "".
5
+ *
6
+ * @param {unknown} value the value to assert
7
+ * @param {string} [label] name used in the thrown error message
8
+ * @returns {string} the value, typed as a string
9
+ */
10
+ export function forcedString(value: unknown, label?: string): string;
11
+ /**
12
+ * Like {@link forcedString}, but allows the value to be absent (null/undefined
13
+ * become null). A value that is present but of the wrong type still throws.
14
+ *
15
+ * @param {unknown} value the value to assert
16
+ * @param {string} [label] name used in the thrown error message
17
+ * @returns {string | null} the string value, or null when absent
18
+ */
19
+ export function optionalString(value: unknown, label?: string): string | null;
20
+ /**
21
+ * Returns the value as an integer, otherwise throws. Numeric strings (route and
22
+ * query params arrive as strings) are parsed; everything else throws.
23
+ *
24
+ * @param {unknown} value the value to assert
25
+ * @param {string} [label] name used in the thrown error message
26
+ * @returns {number} the value, typed as an integer
27
+ */
28
+ export function forcedInteger(value: unknown, label?: string): number;
29
+ /**
30
+ * Like {@link forcedInteger}, but allows the value to be absent (null/undefined
31
+ * become null). A value that is present but not an integer still throws.
32
+ *
33
+ * @param {unknown} value the value to assert
34
+ * @param {string} [label] name used in the thrown error message
35
+ * @returns {number | null} the integer value, or null when absent
36
+ */
37
+ export function optionalInteger(value: unknown, label?: string): number | null;
38
+ /**
39
+ * Returns the value as a finite number, otherwise throws. Numeric strings are
40
+ * parsed; everything else (including NaN and Infinity) throws.
41
+ *
42
+ * @param {unknown} value the value to assert
43
+ * @param {string} [label] name used in the thrown error message
44
+ * @returns {number} the value, typed as a finite number
45
+ */
46
+ export function forcedFloat(value: unknown, label?: string): number;
47
+ /**
48
+ * Like {@link forcedFloat}, but allows the value to be absent (null/undefined
49
+ * become null). A value that is present but not a finite number still throws.
50
+ *
51
+ * @param {unknown} value the value to assert
52
+ * @param {string} [label] name used in the thrown error message
53
+ * @returns {number | null} the number value, or null when absent
54
+ */
55
+ export function optionalFloat(value: unknown, label?: string): number | null;
56
+ /**
57
+ * Returns the value when it is a boolean, otherwise throws. Strings like "true"
58
+ * are intentionally NOT coerced — pass an actual boolean.
59
+ *
60
+ * @param {unknown} value the value to assert
61
+ * @param {string} [label] name used in the thrown error message
62
+ * @returns {boolean} the value, typed as a boolean
63
+ */
64
+ export function forcedBoolean(value: unknown, label?: string): boolean;
65
+ /**
66
+ * Like {@link forcedBoolean}, but allows the value to be absent (null/undefined
67
+ * become null). A value that is present but not a boolean still throws.
68
+ *
69
+ * @param {unknown} value the value to assert
70
+ * @param {string} [label] name used in the thrown error message
71
+ * @returns {boolean | null} the boolean value, or null when absent
72
+ */
73
+ export function optionalBoolean(value: unknown, label?: string): boolean | null;
74
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAgBA;;;;;;;;GAQG;AACH,oCAJW,OAAO,UACP,MAAM,GACJ,MAAM,CAQlB;AAED;;;;;;;GAOG;AACH,sCAJW,OAAO,UACP,MAAM,GACJ,MAAM,GAAG,IAAI,CAMzB;AAED;;;;;;;GAOG;AACH,qCAJW,OAAO,UACP,MAAM,GACJ,MAAM,CAYlB;AAED;;;;;;;GAOG;AACH,uCAJW,OAAO,UACP,MAAM,GACJ,MAAM,GAAG,IAAI,CAMzB;AAED;;;;;;;GAOG;AACH,mCAJW,OAAO,UACP,MAAM,GACJ,MAAM,CAYlB;AAED;;;;;;;GAOG;AACH,qCAJW,OAAO,UACP,MAAM,GACJ,MAAM,GAAG,IAAI,CAMzB;AAED;;;;;;;GAOG;AACH,qCAJW,OAAO,UACP,MAAM,GACJ,OAAO,CAQnB;AAED;;;;;;;GAOG;AACH,uCAJW,OAAO,UACP,MAAM,GACJ,OAAO,GAAG,IAAI,CAM1B"}
package/build/index.js ADDED
@@ -0,0 +1,133 @@
1
+ // @ts-check
2
+ /**
3
+ * Describes the runtime type of a value for error messages, distinguishing the
4
+ * two falsy "absent" values that `typeof` collapses into other buckets.
5
+ *
6
+ * @param {unknown} value the value to describe
7
+ * @returns {string} a short human-readable type label
8
+ */
9
+ function describeType(value) {
10
+ if (value === null)
11
+ return "null";
12
+ if (value === undefined)
13
+ return "undefined";
14
+ return typeof value;
15
+ }
16
+ /**
17
+ * Returns the value when it is a string, otherwise throws. Use this for required
18
+ * values coming from untrusted input (request bodies, webhook payloads, message
19
+ * payloads, parsed config) instead of silently coercing a wrong type to "".
20
+ *
21
+ * @param {unknown} value the value to assert
22
+ * @param {string} [label] name used in the thrown error message
23
+ * @returns {string} the value, typed as a string
24
+ */
25
+ export function forcedString(value, label = "value") {
26
+ if (typeof value !== "string") {
27
+ throw new TypeError(`Expected ${label} to be a string but got ${describeType(value)}`);
28
+ }
29
+ return value;
30
+ }
31
+ /**
32
+ * Like {@link forcedString}, but allows the value to be absent (null/undefined
33
+ * become null). A value that is present but of the wrong type still throws.
34
+ *
35
+ * @param {unknown} value the value to assert
36
+ * @param {string} [label] name used in the thrown error message
37
+ * @returns {string | null} the string value, or null when absent
38
+ */
39
+ export function optionalString(value, label = "value") {
40
+ if (value === null || value === undefined)
41
+ return null;
42
+ return forcedString(value, label);
43
+ }
44
+ /**
45
+ * Returns the value as an integer, otherwise throws. Numeric strings (route and
46
+ * query params arrive as strings) are parsed; everything else throws.
47
+ *
48
+ * @param {unknown} value the value to assert
49
+ * @param {string} [label] name used in the thrown error message
50
+ * @returns {number} the value, typed as an integer
51
+ */
52
+ export function forcedInteger(value, label = "value") {
53
+ if (typeof value === "number" && Number.isInteger(value))
54
+ return value;
55
+ if (typeof value === "string" && value.trim() !== "") {
56
+ const parsedValue = Number(value);
57
+ if (Number.isInteger(parsedValue))
58
+ return parsedValue;
59
+ }
60
+ throw new TypeError(`Expected ${label} to be an integer but got ${describeType(value)}`);
61
+ }
62
+ /**
63
+ * Like {@link forcedInteger}, but allows the value to be absent (null/undefined
64
+ * become null). A value that is present but not an integer still throws.
65
+ *
66
+ * @param {unknown} value the value to assert
67
+ * @param {string} [label] name used in the thrown error message
68
+ * @returns {number | null} the integer value, or null when absent
69
+ */
70
+ export function optionalInteger(value, label = "value") {
71
+ if (value === null || value === undefined)
72
+ return null;
73
+ return forcedInteger(value, label);
74
+ }
75
+ /**
76
+ * Returns the value as a finite number, otherwise throws. Numeric strings are
77
+ * parsed; everything else (including NaN and Infinity) throws.
78
+ *
79
+ * @param {unknown} value the value to assert
80
+ * @param {string} [label] name used in the thrown error message
81
+ * @returns {number} the value, typed as a finite number
82
+ */
83
+ export function forcedFloat(value, label = "value") {
84
+ if (typeof value === "number" && Number.isFinite(value))
85
+ return value;
86
+ if (typeof value === "string" && value.trim() !== "") {
87
+ const parsedValue = Number(value);
88
+ if (Number.isFinite(parsedValue))
89
+ return parsedValue;
90
+ }
91
+ throw new TypeError(`Expected ${label} to be a number but got ${describeType(value)}`);
92
+ }
93
+ /**
94
+ * Like {@link forcedFloat}, but allows the value to be absent (null/undefined
95
+ * become null). A value that is present but not a finite number still throws.
96
+ *
97
+ * @param {unknown} value the value to assert
98
+ * @param {string} [label] name used in the thrown error message
99
+ * @returns {number | null} the number value, or null when absent
100
+ */
101
+ export function optionalFloat(value, label = "value") {
102
+ if (value === null || value === undefined)
103
+ return null;
104
+ return forcedFloat(value, label);
105
+ }
106
+ /**
107
+ * Returns the value when it is a boolean, otherwise throws. Strings like "true"
108
+ * are intentionally NOT coerced — pass an actual boolean.
109
+ *
110
+ * @param {unknown} value the value to assert
111
+ * @param {string} [label] name used in the thrown error message
112
+ * @returns {boolean} the value, typed as a boolean
113
+ */
114
+ export function forcedBoolean(value, label = "value") {
115
+ if (typeof value !== "boolean") {
116
+ throw new TypeError(`Expected ${label} to be a boolean but got ${describeType(value)}`);
117
+ }
118
+ return value;
119
+ }
120
+ /**
121
+ * Like {@link forcedBoolean}, but allows the value to be absent (null/undefined
122
+ * become null). A value that is present but not a boolean still throws.
123
+ *
124
+ * @param {unknown} value the value to assert
125
+ * @param {string} [label] name used in the thrown error message
126
+ * @returns {boolean | null} the boolean value, or null when absent
127
+ */
128
+ export function optionalBoolean(value, label = "value") {
129
+ if (value === null || value === undefined)
130
+ return null;
131
+ return forcedBoolean(value, label);
132
+ }
133
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAAA,YAAY;AAEZ;;;;;;GAMG;AACH,SAAS,YAAY,CAAC,KAAK;IACzB,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAA;IACjC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,WAAW,CAAA;IAE3C,OAAO,OAAO,KAAK,CAAA;AACrB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;IACjD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,SAAS,CAAC,YAAY,KAAK,2BAA2B,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IACxF,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;IACnD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAEtD,OAAO,YAAY,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;AACnC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAEtE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAEjC,IAAI,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC;YAAE,OAAO,WAAW,CAAA;IACvD,CAAC;IAED,MAAM,IAAI,SAAS,CAAC,YAAY,KAAK,6BAA6B,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;AAC1F,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;IACpD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAEtD,OAAO,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;AACpC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;IAChD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAErE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAEjC,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;YAAE,OAAO,WAAW,CAAA;IACtD,CAAC;IAED,MAAM,IAAI,SAAS,CAAC,YAAY,KAAK,2BAA2B,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;AACxF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;IAClD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAEtD,OAAO,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;AAClC,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;IAClD,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,MAAM,IAAI,SAAS,CAAC,YAAY,KAAK,4BAA4B,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IACzF,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO;IACpD,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IAEtD,OAAO,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;AACpC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "typanic",
3
+ "version": "1.0.1",
4
+ "description": "Forced runtime type assertions for untrusted input — get the type you expect or throw, instead of silently coercing a wrong value to \"\" / 0 / false.",
5
+ "keywords": [
6
+ "type",
7
+ "types",
8
+ "assert",
9
+ "assertion",
10
+ "validate",
11
+ "validation",
12
+ "cast",
13
+ "coerce",
14
+ "runtime",
15
+ "typecheck",
16
+ "guard",
17
+ "jsdoc",
18
+ "string",
19
+ "integer",
20
+ "number",
21
+ "boolean",
22
+ "throw"
23
+ ],
24
+ "license": "MIT",
25
+ "author": "kaspernj <kasper@diestoeckels.de>",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/kaspernj/typanic.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/kaspernj/typanic/issues"
32
+ },
33
+ "homepage": "https://github.com/kaspernj/typanic#readme",
34
+ "type": "module",
35
+ "main": "build/index.js",
36
+ "types": "build/index.d.ts",
37
+ "exports": {
38
+ ".": {
39
+ "types": "./build/index.d.ts",
40
+ "import": "./build/index.js"
41
+ }
42
+ },
43
+ "files": [
44
+ "build",
45
+ "README.md",
46
+ "LICENSE"
47
+ ],
48
+ "scripts": {
49
+ "all-checks": "npm run lint && npm run typecheck && npm test",
50
+ "build": "tsc --project tsconfig.json",
51
+ "lint": "eslint .",
52
+ "prepare": "npm run build",
53
+ "release:patch": "release-patch",
54
+ "test": "jasmine",
55
+ "typecheck": "tsc --project tsconfig.json --noEmit"
56
+ },
57
+ "devDependencies": {
58
+ "@eslint/js": "^10.0.1",
59
+ "@types/node": "^25.6.0",
60
+ "eslint": "^10.3.0",
61
+ "eslint-plugin-jsdoc": "^62.9.0",
62
+ "globals": "^17.6.0",
63
+ "jasmine": "^6.2.0",
64
+ "release-patch": "^1.0.0",
65
+ "typescript": "^5.9.3"
66
+ }
67
+ }