@jakobkg/shapes-ts 0.1.0

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,19 @@
1
+ Copyright (c) 2025 Jakob Grønhaug.
2
+
3
+ Redistribution and use in source and binary forms, with or without modification,
4
+ are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+ 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+ 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9
+
10
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
11
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
12
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
13
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
14
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
15
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
16
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
17
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
18
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
19
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # shapes-ts
2
+
3
+ Toy library to create runtime safe types in TS, see `main.ts` for example usage
@@ -0,0 +1,2 @@
1
+ export * as Shapes from "./shapes";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/shapes/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Shapes = void 0;
4
+ exports.Shapes = require("./shapes");
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Bridge to convert a Shape into a TS type.
3
+ */
4
+ export type Type<T extends Shape> = T["primitive"];
5
+ type ShapePredicate<T> = (x: T) => boolean;
6
+ interface Shape<T = any> {
7
+ readonly typename: string;
8
+ readonly primitive: T;
9
+ readonly optional?: boolean;
10
+ readonly predicates: ShapePredicate<T>[];
11
+ check(x: unknown): x is T;
12
+ /**
13
+ * Adds a custom validation predicate to the shape
14
+ * @param predicate A function that takes a value of the shape's type and returns a boolean
15
+ * @param description Optional description of the constraint for error messages
16
+ * @returns A new shape with the additional validation
17
+ */
18
+ where(predicate: ShapePredicate<T>, description: string): Shape<T>;
19
+ and<U>(other: Shape<U>): IntersectionShape<T, U>;
20
+ or<U>(other: Shape<U>): UnionShape<T, U>;
21
+ }
22
+ interface ObjectShape<T = Record<string, unknown>> extends Shape<T> {
23
+ readonly properties: Record<string, Shape<T>>;
24
+ readonly allowUnknownProperties?: boolean;
25
+ }
26
+ interface ObjectShapeOptions {
27
+ readonly allowUnknownProperties?: boolean;
28
+ readonly additionalPermittedProperties?: Record<string, Shape<Record<string, unknown>>>;
29
+ }
30
+ interface ArrayShape<T extends Shape> extends Shape<Array<T["primitive"]>> {
31
+ readonly inner: T;
32
+ }
33
+ interface UnionShape<Left, Right> extends Shape<Left | Right> {
34
+ left: Shape<Left>;
35
+ right: Shape<Right>;
36
+ }
37
+ interface IntersectionShape<Left, Right> extends Shape<Left & Right> {
38
+ left: Shape<Left>;
39
+ right: Shape<Right>;
40
+ }
41
+ interface OptionalShape<T extends Shape> extends Shape<T["primitive"] | undefined> {
42
+ readonly inner: T;
43
+ }
44
+ interface NullableShape<T extends Shape> extends Shape<T["primitive"] | null> {
45
+ readonly inner: T;
46
+ }
47
+ /**
48
+ * Creates a shape representing a JS `number`.
49
+ * Does not support BigInt.
50
+ */
51
+ export declare function number(): Shape<number>;
52
+ /**
53
+ * Creates a shape representing a JS `string`.
54
+ */
55
+ export declare function string(): Shape<string>;
56
+ /**
57
+ * Creates a shape representing a JS `boolean`.
58
+ */
59
+ export declare function boolean(): Shape<boolean>;
60
+ /**
61
+ * Creates an array shape, representing `Array<T>` (aka `T[]`).
62
+ * Takes the shape representing `T` as a parameter.
63
+ */
64
+ export declare function array<T extends Shape>(shape: T): ArrayShape<T>;
65
+ /**
66
+ * Creates an object shape.
67
+ *
68
+ * @example
69
+ * const Group = Shapes.object({
70
+ * id: Shapes.number(),
71
+ * name: Shapes.string(),
72
+ * });
73
+ *
74
+ * const User = Shapes.object({
75
+ * id: Shapes.number(),
76
+ * name: Shapes.string(),
77
+ * groups: Shapes.nullable(
78
+ * Shapes.array(Group)
79
+ * )
80
+ * });
81
+ */
82
+ export declare function object<T extends Record<string, Shape>>(properties: T, options?: ObjectShapeOptions): ObjectShape<{
83
+ [K in keyof T]: Type<T[K]>;
84
+ }>;
85
+ /**
86
+ * Creates a shape representing an optional type.
87
+ * `Shapes.optional(T)` corresponds to `T | undefined`
88
+ */
89
+ export declare function optional<T extends Shape>(shape: T): OptionalShape<T>;
90
+ /**
91
+ * Creates a shape representing a nullable type.
92
+ * `Shapes.nullable(T)` corresponds to `T | null`
93
+ */
94
+ export declare function nullable<T extends Shape>(shape: T): NullableShape<T>;
95
+ export {};
96
+ //# sourceMappingURL=shapes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shapes.d.ts","sourceRoot":"","sources":["../src/shapes/shapes.ts"],"names":[],"mappings":"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"}
package/dist/shapes.js ADDED
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ /// TYPE GUARDS
3
+ // These are static functions to avoid allocating guards per Shape instance,
4
+ // Shapes may reference these instead
5
+ var __assign = (this && this.__assign) || function () {
6
+ __assign = Object.assign || function(t) {
7
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
8
+ s = arguments[i];
9
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
10
+ t[p] = s[p];
11
+ }
12
+ return t;
13
+ };
14
+ return __assign.apply(this, arguments);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.number = number;
18
+ exports.string = string;
19
+ exports.boolean = boolean;
20
+ exports.array = array;
21
+ exports.object = object;
22
+ exports.optional = optional;
23
+ exports.nullable = nullable;
24
+ function isString(input) {
25
+ return typeof input === "string";
26
+ }
27
+ function isNumber(input) {
28
+ return Number.isFinite(input);
29
+ }
30
+ function isObject(input) {
31
+ return input !== null && typeof input === "object" && !Array.isArray(input);
32
+ }
33
+ function isArray(input) {
34
+ return Array.isArray(input);
35
+ }
36
+ function isNull(input) {
37
+ return input === null;
38
+ }
39
+ function isBoolean(input) {
40
+ return typeof input === "boolean";
41
+ }
42
+ function isUndefined(input) {
43
+ return input === undefined;
44
+ }
45
+ function makeUnion(left, right) {
46
+ return {
47
+ typename: "".concat(left.typename, " | ").concat(right.typename),
48
+ primitive: undefined,
49
+ left: left,
50
+ right: right,
51
+ predicates: [],
52
+ where: function (p) {
53
+ this.predicates.push(p);
54
+ return this;
55
+ },
56
+ check: function (x) {
57
+ return (left.check(x) || right.check(x)) &&
58
+ this.predicates.every(function (p) { return p(x); });
59
+ }, // TODO: refine for objects?
60
+ and: function (other) {
61
+ return makeIntersection(this, other);
62
+ },
63
+ or: function (other) {
64
+ return makeUnion(this, other);
65
+ },
66
+ };
67
+ }
68
+ function makeIntersection(left, right) {
69
+ return {
70
+ typename: "".concat(left.typename, " | ").concat(right.typename),
71
+ primitive: undefined,
72
+ left: __assign(__assign({}, left), {
73
+ // Trickery: combine permitted properties of left and right
74
+ properties: __assign(__assign({}, right.properties), left.properties) }),
75
+ right: __assign(__assign({}, right), {
76
+ // Trickery: combine permitted properties of left and right
77
+ properties: __assign(__assign({}, left.properties), right.properties) }),
78
+ predicates: [],
79
+ where: function (p) {
80
+ this.predicates.push(p);
81
+ return this;
82
+ },
83
+ check: function (x) {
84
+ return this.left.check(x) && this.right.check(x) &&
85
+ this.predicates.every(function (p) { return p(x); });
86
+ },
87
+ and: function (other) {
88
+ return makeIntersection(this, other);
89
+ },
90
+ or: function (other) {
91
+ return makeUnion(this, other);
92
+ },
93
+ };
94
+ }
95
+ /// PRIMITIVE SHAPE FACTORIES
96
+ /**
97
+ * Creates a shape representing a JS `number`.
98
+ * Does not support BigInt.
99
+ */
100
+ function number() {
101
+ return {
102
+ typename: "number",
103
+ predicates: [],
104
+ primitive: undefined,
105
+ check: function (x) {
106
+ return isNumber(x) && this.predicates.every(function (p) { return p(x); });
107
+ },
108
+ where: function (p) {
109
+ this.predicates.push(p);
110
+ return this;
111
+ },
112
+ and: function (other) {
113
+ return makeIntersection(this, other);
114
+ },
115
+ or: function (other) {
116
+ return makeUnion(this, other);
117
+ },
118
+ };
119
+ }
120
+ /**
121
+ * Creates a shape representing a JS `string`.
122
+ */
123
+ function string() {
124
+ return {
125
+ typename: "string",
126
+ primitive: undefined,
127
+ check: function (x) {
128
+ return isString(x) && this.predicates.every(function (p) { return p(x); });
129
+ },
130
+ predicates: [],
131
+ where: function (predicate) {
132
+ this.predicates.push(predicate);
133
+ return this;
134
+ },
135
+ and: function (other) {
136
+ return makeIntersection(this, other);
137
+ },
138
+ or: function (other) {
139
+ return makeUnion(this, other);
140
+ },
141
+ };
142
+ }
143
+ /**
144
+ * Creates a shape representing a JS `boolean`.
145
+ */
146
+ function boolean() {
147
+ return {
148
+ primitive: undefined,
149
+ typename: "boolean",
150
+ check: function (x) {
151
+ return isBoolean(x) && this.predicates.every(function (p) { return p(x); });
152
+ },
153
+ predicates: [],
154
+ where: function (predicate) {
155
+ this.predicates.push(predicate);
156
+ return this;
157
+ },
158
+ and: function (other) {
159
+ return makeIntersection(this, other);
160
+ },
161
+ or: function (other) {
162
+ return makeUnion(this, other);
163
+ },
164
+ };
165
+ }
166
+ /// COMPLEX SHAPE FACTORIES
167
+ /**
168
+ * Creates an array shape, representing `Array<T>` (aka `T[]`).
169
+ * Takes the shape representing `T` as a parameter.
170
+ */
171
+ function array(shape) {
172
+ var typename = "Array<".concat(shape.typename, ">");
173
+ return {
174
+ typename: typename,
175
+ inner: shape,
176
+ primitive: undefined,
177
+ check: function (input) {
178
+ if (!isArray(input)) {
179
+ return false;
180
+ }
181
+ return input.every(function (entry) {
182
+ if (!shape.check(entry)) {
183
+ return false;
184
+ }
185
+ return true;
186
+ }) && this.predicates.every(function (p) { return p(input); });
187
+ },
188
+ predicates: [],
189
+ where: function (predicate) {
190
+ this.predicates.push(predicate);
191
+ return this;
192
+ },
193
+ and: function (other) {
194
+ return makeIntersection(this, other);
195
+ },
196
+ or: function (other) {
197
+ return makeUnion(this, other);
198
+ },
199
+ };
200
+ }
201
+ /**
202
+ * Creates an object shape.
203
+ *
204
+ * @example
205
+ * const Group = Shapes.object({
206
+ * id: Shapes.number(),
207
+ * name: Shapes.string(),
208
+ * });
209
+ *
210
+ * const User = Shapes.object({
211
+ * id: Shapes.number(),
212
+ * name: Shapes.string(),
213
+ * groups: Shapes.nullable(
214
+ * Shapes.array(Group)
215
+ * )
216
+ * });
217
+ */
218
+ function object(properties, options) {
219
+ var allowUnknownProperties = options === null || options === void 0 ? void 0 : options.allowUnknownProperties;
220
+ return {
221
+ typename: "Object",
222
+ allowUnknownProperties: allowUnknownProperties,
223
+ check: function (input) {
224
+ // Check that input is an object
225
+ if (!isObject(input)) {
226
+ return false;
227
+ }
228
+ // Check for unknown properties
229
+ if (!this.allowUnknownProperties) {
230
+ for (var inputProperty in input) {
231
+ if (!(inputProperty in this.properties)) {
232
+ return false;
233
+ }
234
+ }
235
+ }
236
+ // Check required properties and their types
237
+ for (var property in properties) {
238
+ var field = input[property];
239
+ var propertyShape = properties[property];
240
+ // Property presence check
241
+ if (!(property in input) && !propertyShape.optional) {
242
+ return false;
243
+ }
244
+ // Property type check
245
+ if (!propertyShape.check(field)) {
246
+ return false;
247
+ }
248
+ }
249
+ return this.predicates.every(function (p) { return p(input); });
250
+ },
251
+ properties: __assign(__assign({}, properties), options === null || options === void 0 ? void 0 : options.additionalPermittedProperties),
252
+ primitive: undefined,
253
+ predicates: [],
254
+ where: function (predicate) {
255
+ this.predicates.push(predicate);
256
+ return this;
257
+ },
258
+ and: function (other) {
259
+ return makeIntersection(this, other);
260
+ },
261
+ or: function (other) {
262
+ return makeUnion(this, other);
263
+ },
264
+ };
265
+ }
266
+ /// TYPE MODIFIERS
267
+ /**
268
+ * Creates a shape representing an optional type.
269
+ * `Shapes.optional(T)` corresponds to `T | undefined`
270
+ */
271
+ function optional(shape) {
272
+ return {
273
+ typename: "".concat(shape.typename, " | undefined"),
274
+ inner: shape,
275
+ optional: true,
276
+ primitive: undefined,
277
+ predicates: [],
278
+ check: function (input) {
279
+ return isUndefined(input) || shape.check(input);
280
+ },
281
+ where: function (predicate) {
282
+ this.inner.predicates.push(predicate);
283
+ return this;
284
+ },
285
+ and: function (other) {
286
+ return makeIntersection(this, other);
287
+ },
288
+ or: function (other) {
289
+ return makeUnion(this, other);
290
+ },
291
+ };
292
+ }
293
+ /**
294
+ * Creates a shape representing a nullable type.
295
+ * `Shapes.nullable(T)` corresponds to `T | null`
296
+ */
297
+ function nullable(shape) {
298
+ var nullShape = {
299
+ typename: "".concat(shape.typename, " | null"),
300
+ check: function (input) {
301
+ return isNull(input) || shape.check(input);
302
+ },
303
+ inner: shape,
304
+ primitive: undefined,
305
+ predicates: [],
306
+ where: function (p) {
307
+ this.inner.predicates.push(p);
308
+ return this;
309
+ },
310
+ and: function (other) {
311
+ return makeIntersection(this, other);
312
+ },
313
+ or: function (other) {
314
+ return makeUnion(this, other);
315
+ },
316
+ };
317
+ return nullShape;
318
+ }
Binary file
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@jakobkg/shapes-ts",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "test": "deno test",
8
+ "test:coverage": "deno test --coverage",
9
+ "build": "tsc"
10
+ },
11
+ "devDependencies": {
12
+ "typescript": "5.9.3"
13
+ }
14
+ }
Binary file