@nerd-bible/valio 0.0.12 → 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.
@@ -0,0 +1,26 @@
1
+ name: Publish
2
+ on:
3
+ push:
4
+ tags:
5
+ - v[0-9]+.[0-9]+.[0-9]+
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ with:
19
+ fetch-depth: 0
20
+ - uses: actions/setup-node@v6
21
+ with:
22
+ node-version: lts/*
23
+ registry-url: 'https://registry.npmjs.org'
24
+ - run: npm install
25
+ - run: npm test
26
+ - run: node ./publish.ts "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY"
@@ -0,0 +1,14 @@
1
+ name: Test
2
+ on:
3
+ push:
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - uses: actions/setup-node@v6
11
+ with:
12
+ node-version: lts/*
13
+ - run: npm install
14
+ - run: npm test
package/README.md CHANGED
@@ -35,8 +35,8 @@ const schema = v.number(); // Pipe<number, number>
35
35
 
36
36
  expect(schema.decode(5)).toEqual({ success: true, output: 5 });
37
37
  expect(schema.decodeAny("5")).toEqual({
38
- success: false,
39
- errors: { ".": [{ input: "5", message: "not type number" }] },
38
+ success: false,
39
+ errors: { ".": [{ input: "5", message: "not type number" }] },
40
40
  });
41
41
  ```
42
42
 
package/package.json CHANGED
@@ -1,27 +1,26 @@
1
1
  {
2
2
  "name": "@nerd-bible/valio",
3
3
  "type": "module",
4
- "main": "dist/index.js",
5
- "types": "dist/index.d.ts",
6
- "devDependencies": {
7
- "@biomejs/biome": "^2.3.1",
8
- "@types/bun": "latest"
4
+ "exports": {
5
+ ".": {
6
+ "import": {
7
+ "nbsource": "./src/index.ts",
8
+ "node": "./dist/index.js"
9
+ }
10
+ },
11
+ "./package.json": "./package.json"
9
12
  },
10
13
  "scripts": {
11
- "build": "tsc",
12
- "fmt": "biome check",
13
- "fmt-fix": "biome check --write --unsafe"
14
+ "build": "tsc -p ./tsconfig.build.json",
15
+ "test": "node --test"
14
16
  },
15
- "peerDependencies": {
17
+ "devDependencies": {
18
+ "@types/node": "^25.3.3",
19
+ "expect": "^30.2.0",
16
20
  "typescript": "^5.9.3"
17
21
  },
18
- "files": [
19
- "README.md",
20
- "src",
21
- "dist"
22
- ],
23
- "version": "0.0.12",
22
+ "version": "0.1.0",
24
23
  "repository": {
25
- "url": "git+https://github.com/nerd-bible/valio"
24
+ "url": "https://github.com/nerd-bible/valio"
26
25
  }
27
- }
26
+ }
package/publish.ts ADDED
@@ -0,0 +1,52 @@
1
+ // This script allows using Git tags as the source of truth for versions rather
2
+ // than package.json.
3
+ import { execSync } from "node:child_process";
4
+ import { readFileSync, writeFileSync } from "node:fs";
5
+
6
+ const repoUrl = process.argv.slice(2).find((a) => !a.startsWith("--"));
7
+ if (!repoUrl) throw Error("Required arg: repo URL");
8
+ const dry = process.argv.includes("--dry");
9
+
10
+ function execGit(args: string, allowFailure = false) {
11
+ try {
12
+ return execSync(`git ${args}`, {
13
+ encoding: "utf8",
14
+ stdio: ["ignore", "pipe", "ignore"],
15
+ }).trim();
16
+ } catch (err) {
17
+ if (!allowFailure) throw err;
18
+ }
19
+ }
20
+
21
+ execGit("fetch --tags");
22
+ const tagCmd = "describe --tags --abbrev=0 --match='v[0-9]*.[0-9]*.[0-9]*'";
23
+ let version = execGit(`${tagCmd} --exact-match`, true);
24
+
25
+ if (!version) {
26
+ const lastVersion = execGit(tagCmd, true) ?? "v0.0.0";
27
+ console.log("No manual tag, bumping", lastVersion);
28
+
29
+ const split = lastVersion.split(".");
30
+ // Tags should only be missing for minor bumps.
31
+ const last = parseInt(split.pop()!) + 1;
32
+ version = [...split, last].join(".");
33
+
34
+ if (!dry) {
35
+ console.log("Tagging + pushing", version);
36
+ execGit(`tag ${version}`);
37
+ execGit("push --tags origin master");
38
+ }
39
+ } else {
40
+ console.log("Using manual tag", version);
41
+ }
42
+
43
+ if (!dry) {
44
+ console.log("Writing temporary package.json");
45
+ const pkg = JSON.parse(readFileSync("package.json", "utf8"));
46
+ pkg.version = version.substring(1);
47
+ pkg.repository = { url: repoUrl };
48
+ writeFileSync("package.json", JSON.stringify(pkg, null, 2));
49
+
50
+ console.log("Publishing to NPM", version);
51
+ execSync("npm publish --access public");
52
+ }
@@ -1,5 +1,6 @@
1
- import { expect, test } from "bun:test";
2
- import * as v from "./index";
1
+ import { test } from "node:test";
2
+ import expect from "expect";
3
+ import * as v from "./index.ts";
3
4
 
4
5
  test("number codec", () => {
5
6
  const schema = v.codecs.number();
@@ -65,7 +66,7 @@ test("array number codec", () => {
65
66
  });
66
67
 
67
68
  test("boolean codec", () => {
68
- const schema = v.codecs.boolean({ true: ["yes"], false: ["no"] });
69
+ const schema = v.codecs.boolean({ true: /yes|^[1-9]/, false: /no|^0?$/ });
69
70
 
70
71
  expect(schema.decode("yes")).toEqual({ success: true, output: true });
71
72
  expect(schema.decode("no")).toEqual({ success: true, output: false });
package/src/codecs.ts CHANGED
@@ -1,6 +1,5 @@
1
- import * as c from "./containers";
2
- import type { Context, Pipe, Result } from "./pipe";
3
- import * as p from "./primitives";
1
+ import type { Context, Pipe, Result } from "./pipe.ts";
2
+ import * as p from "./primitives.ts";
4
3
 
5
4
  export function custom<I, O>(
6
5
  input: Pipe<I, any>,
@@ -20,37 +19,37 @@ export function custom<I, O>(
20
19
  export function number(
21
20
  parser = Number.parseFloat,
22
21
  ): p.Comparable<string | number | null | undefined, number> {
23
- return custom(
24
- c.union([p.string(), p.number(), p.null(), p.undefined()]),
25
- p.number(),
26
- {
27
- decode(input, ctx) {
28
- if (typeof input == "number") return { success: true, output: input };
29
- if (input == null || input.toLowerCase() == "nan")
30
- return { success: true, output: Number.NaN };
22
+ return custom(p.any(), p.number(), {
23
+ decode(input, ctx) {
24
+ if (typeof input === "number") return { success: true, output: input };
25
+ if (!input || input.toLowerCase() === "nan")
26
+ return { success: true, output: Number.NaN };
31
27
 
32
- const output = parser(input);
33
- if (!Number.isNaN(output)) return { success: true, output };
28
+ const output = parser(input);
29
+ if (!Number.isNaN(output)) return { success: true, output };
34
30
 
35
- ctx.pushErrorFmt("coerce", input, { expected: "number" });
36
- return { success: false, errors: ctx.errors };
37
- },
31
+ ctx.pushErrorFmt("coerce", input, { expected: "number" });
32
+ return { success: false, errors: ctx.errors };
38
33
  },
39
- ) as ReturnType<typeof number>;
34
+ }) as ReturnType<typeof number>;
40
35
  }
41
36
 
42
37
  export function boolean(opts: {
43
- true?: string[];
44
- false?: string[];
38
+ true?: RegExp;
39
+ false?: RegExp;
45
40
  }): Pipe<any, boolean> {
46
41
  return custom(p.any(), p.boolean(), {
47
- decode(input) {
42
+ decode(input, ctx) {
43
+ if (typeof input === "boolean") return { success: true, output: input };
48
44
  if (typeof input === "string") {
49
- if (opts.true?.includes(input)) return { success: true, output: true };
50
- if (opts.false?.includes(input))
51
- return { success: true, output: false };
45
+ if (opts.true?.test(input)) return { success: true, output: true };
46
+ if (opts.false?.test(input)) return { success: true, output: false };
52
47
  }
53
- return { success: true, output: Boolean(input) };
48
+ if (typeof input === "number")
49
+ return { success: true, output: Boolean(input) };
50
+
51
+ ctx.pushErrorFmt("coerce", input, { expected: "boolean" });
52
+ return { success: false, errors: ctx.errors };
54
53
  },
55
54
  });
56
55
  }
@@ -1,5 +1,6 @@
1
- import { expect, test } from "bun:test";
2
- import * as v from "./index";
1
+ import { test } from "node:test";
2
+ import expect from "expect";
3
+ import * as v from "./index.ts";
3
4
 
4
5
  test("array", () => {
5
6
  const schema = v.array(v.number());
package/src/containers.ts CHANGED
@@ -1,14 +1,20 @@
1
- import type { Input, Output, Result } from "./pipe";
2
- import { type Context, HalfPipe, Pipe } from "./pipe";
3
- import * as p from "./primitives";
1
+ import type { Input, Output, Result } from "./pipe.ts";
2
+ import { Context, HalfPipe, Pipe } from "./pipe.ts";
3
+ import * as p from "./primitives.ts";
4
4
 
5
5
  class ValioArray<T> extends p.Arrayish<any[], T[]> {
6
- constructor(public element: Pipe<any, T>) {
6
+ element: Pipe<any, T>;
7
+
8
+ static typeCheck(v: any): v is any[] {
9
+ return Array.isArray(v);
10
+ }
11
+
12
+ constructor(element: Pipe<any, T>) {
7
13
  super(
8
14
  new HalfPipe(
9
15
  "array",
10
- (v: any): v is any[] => Array.isArray(v),
11
- (input: any[], ctx: Context): Result<T[]> => {
16
+ ValioArray.typeCheck,
17
+ function parseAnyArr(input: any[], ctx: Context): Result<T[]> {
12
18
  const output = new Array<T>(input.length);
13
19
  let success = true;
14
20
 
@@ -25,12 +31,16 @@ class ValioArray<T> extends p.Arrayish<any[], T[]> {
25
31
  return { success, output };
26
32
  },
27
33
  ),
28
- new HalfPipe(`array<${element.o.name}>`, (v: any): v is T[] => {
29
- if (!Array.isArray(v)) return false;
30
- for (const e of v) if (!element.o.typeCheck(e)) return false;
31
- return true;
32
- }),
34
+ new HalfPipe(
35
+ `array<${element.o.name}>`,
36
+ function isArrT(v: any): v is T[] {
37
+ if (!ValioArray.typeCheck(v)) return false;
38
+ for (const e of v) if (!element.o.typeCheck(e)) return false;
39
+ return true;
40
+ },
41
+ ),
33
42
  );
43
+ this.element = element;
34
44
  }
35
45
  }
36
46
  export function array<T>(element: Pipe<any, T>): ValioArray<T> {
@@ -41,16 +51,22 @@ class ValioRecord<K extends PropertyKey, V> extends Pipe<
41
51
  Record<any, any>,
42
52
  Record<K, V>
43
53
  > {
44
- constructor(
45
- public keyPipe: Pipe<any, K>,
46
- public valPipe: Pipe<any, V>,
47
- ) {
54
+ keyPipe: Pipe<any, K>;
55
+ valPipe: Pipe<any, V>;
56
+
57
+ static typeCheck(v: any): v is Record<any, any> {
58
+ return Object.prototype.toString.call(v) === "[object Object]";
59
+ }
60
+
61
+ constructor(keyPipe: Pipe<any, K>, valPipe: Pipe<any, V>) {
48
62
  super(
49
63
  new HalfPipe(
50
64
  "object",
51
- (v): v is Record<any, any> =>
52
- Object.prototype.toString.call(v) == "[object Object]",
53
- (input: Record<any, any>, ctx: Context): Result<Record<K, V>> => {
65
+ ValioRecord.typeCheck,
66
+ function anyToRecordKV(
67
+ input: Record<any, any>,
68
+ ctx: Context,
69
+ ): Result<Record<K, V>> {
54
70
  const output = {} as Record<K, V>;
55
71
 
56
72
  let success = true;
@@ -77,9 +93,8 @@ class ValioRecord<K extends PropertyKey, V> extends Pipe<
77
93
  ),
78
94
  new HalfPipe(
79
95
  `record<${keyPipe.o.name},${valPipe.o.name}>`,
80
- (v): v is Record<K, V> => {
81
- if (Object.prototype.toString.call(v) != "[object Object]")
82
- return false;
96
+ function recordCheckV(v): v is Record<K, V> {
97
+ if (!ValioRecord.typeCheck(v)) return false;
83
98
  for (const k in v) {
84
99
  // Keys will always be strings.
85
100
  // if (!keyPipe.o.typeCheck(k)) return false;
@@ -89,6 +104,8 @@ class ValioRecord<K extends PropertyKey, V> extends Pipe<
89
104
  },
90
105
  ),
91
106
  );
107
+ this.keyPipe = keyPipe;
108
+ this.valPipe = valPipe;
92
109
  }
93
110
  }
94
111
  export function record<K extends PropertyKey, V>(
@@ -102,32 +119,39 @@ class Union<T extends Readonly<Pipe[]>> extends Pipe<
102
119
  Output<T[number]>,
103
120
  Output<T[number]>
104
121
  > {
105
- constructor(public options: T) {
122
+ options: T;
123
+
124
+ constructor(options: T) {
106
125
  const name = options.map((o) => o.o.name).join("|");
107
126
  type O = Output<T[number]>;
108
127
  super(
109
128
  new HalfPipe(
110
129
  name,
111
- (v: any): v is O => {
130
+ function isUnionType(v: any): v is O {
112
131
  for (const f of options) if (f.i.typeCheck(v)) return true;
113
132
  return false;
114
133
  },
115
134
  (data: O, ctx: Context): Result<O> => {
116
- const newCtx = ctx.clone();
117
- for (const s in options) {
118
- const decoded = options[s]!.decode(data, newCtx);
135
+ // Throw away errors since we expect them.
136
+ const newCtx = new Context();
137
+ newCtx.pushErrorFmt = () => {};
138
+ newCtx.pushError = () => {};
139
+ for (const f of options) {
140
+ const decoded = f.decode(data, newCtx);
119
141
  if (decoded.success) return decoded;
120
142
  }
121
143
 
122
- Object.assign(ctx.errors, newCtx.errors);
144
+ // Sad path -- do again with real ctx to gather errors.
145
+ for (const f of options) f.decode(data, ctx);
123
146
  return { success: false, errors: ctx.errors };
124
147
  },
125
148
  ),
126
- new HalfPipe(name, (v: any): v is O => {
149
+ new HalfPipe(name, function isUnionType2(v: any): v is O {
127
150
  for (const f of options) if (f.o.typeCheck(v)) return true;
128
151
  return false;
129
152
  }),
130
153
  );
154
+ this.options = options;
131
155
  }
132
156
  }
133
157
  export function union<T extends Readonly<Pipe[]>>(options: T): Union<T> {
@@ -151,19 +175,21 @@ type Extend<A extends Record<any, any>, B extends Record<any, any>> = Flatten<
151
175
  }
152
176
  >;
153
177
 
154
- class ValioObject<Shape extends Record<any, Pipe<any, any>>> extends Pipe<
155
- Record<any, any>,
156
- ObjectOutput<Shape>
157
- > {
158
- constructor(
159
- public shape: Shape,
160
- public isLoose: boolean,
161
- ) {
178
+ export class ValioObject<
179
+ Shape extends Record<any, Pipe<any, any>>,
180
+ > extends Pipe<Record<any, any>, ObjectOutput<Shape>> {
181
+ shape: Shape;
182
+ isLoose: boolean;
183
+
184
+ static typeCheck(v: any): v is Record<any, any> {
185
+ return typeof v === "object";
186
+ }
187
+
188
+ constructor(shape: Shape, isLoose: boolean) {
162
189
  super(
163
190
  new HalfPipe(
164
191
  "object",
165
- (v): v is Record<any, any> =>
166
- Object.prototype.toString.call(v) == "[object Object]",
192
+ ValioObject.typeCheck,
167
193
  (data, ctx) => this.transformInput(data, ctx),
168
194
  ),
169
195
  new HalfPipe(
@@ -173,6 +199,9 @@ class ValioObject<Shape extends Record<any, Pipe<any, any>>> extends Pipe<
173
199
  (v) => this.typeCheckOutput(v),
174
200
  ),
175
201
  );
202
+
203
+ this.shape = shape;
204
+ this.isLoose = isLoose;
176
205
  }
177
206
 
178
207
  clone(): this {
@@ -204,7 +233,7 @@ class ValioObject<Shape extends Record<any, Pipe<any, any>>> extends Pipe<
204
233
  }
205
234
 
206
235
  protected typeCheckOutput(v: any): v is ObjectOutput<Shape> {
207
- if (Object.prototype.toString.call(v) != "[object Object]") return false;
236
+ if (!ValioObject.typeCheck(v)) return false;
208
237
  for (const s in this.shape)
209
238
  if (!this.shape[s]!.o.typeCheck(v[s])) return false;
210
239
  return true;
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export * as codecs from "./codecs";
2
- export * from "./containers";
3
- export * from "./pipe";
4
- export * from "./primitives";
1
+ export * as codecs from "./codecs.ts";
2
+ export * from "./containers.ts";
3
+ export * from "./pipe.ts";
4
+ export * from "./primitives.ts";
package/src/pipe.ts CHANGED
@@ -1,4 +1,4 @@
1
- import enFormat from "./locales/en";
1
+ import enFormat from "./locales/en.ts";
2
2
 
3
3
  export type Error = { input: any; message: string };
4
4
  export type Errors = { [inputPath: string]: Error[] };
@@ -20,21 +20,31 @@ interface Check<T> {
20
20
  }
21
21
 
22
22
  export class HalfPipe<I, O = never> {
23
- constructor(
24
- /** The type name */
25
- public name: string,
26
- /** The first check to run */
27
- public typeCheck: (v: any) => v is I,
28
- /** Optional transform for pipe to run at end. Useful for containers */
29
- public transform?: (v: I, ctx: Context) => Result<O>,
30
- ) {}
31
- /** The second checks to run */
23
+ name: string;
24
+ typeCheck: (v: any) => v is I;
25
+ transform?: (v: I, ctx: Context) => Result<O>;
26
+ /** Checks to run after type check */
32
27
  checks: Check<I>[] = [];
33
28
 
34
- clone(): this {
35
- const res = clone(this);
36
- res.checks = res.checks.slice();
37
- return res;
29
+ constructor(
30
+ name: string,
31
+ typeCheck: (v: any) => v is I,
32
+ transform?: (v: I, ctx: Context) => Result<O>,
33
+ checks: Check<I>[] = [],
34
+ ) {
35
+ this.name = name;
36
+ this.typeCheck = typeCheck;
37
+ this.transform = transform;
38
+ this.checks = checks;
39
+ }
40
+
41
+ clone(): HalfPipe<I, O> {
42
+ return new HalfPipe(
43
+ this.name,
44
+ this.typeCheck,
45
+ this.transform,
46
+ this.checks.slice(),
47
+ );
38
48
  }
39
49
  }
40
50
 
@@ -48,9 +58,9 @@ export class Context {
48
58
  }
49
59
 
50
60
  clone(): Context {
51
- const res = clone(this);
52
- res.jsonPath = res.jsonPath.slice();
53
- res.errors = { ...res.errors };
61
+ const res = new Context();
62
+ res.jsonPath = this.jsonPath.slice();
63
+ res.errors = { ...this.errors };
54
64
  return res;
55
65
  }
56
66
 
@@ -83,13 +93,17 @@ export class Context {
83
93
  }
84
94
 
85
95
  export class Pipe<I = any, O = any> {
86
- constructor(
87
- public i: HalfPipe<I, O>,
88
- public o: HalfPipe<O, I>,
89
- ) {}
96
+ i: HalfPipe<I, O>;
97
+ o: HalfPipe<O, I>;
98
+
99
+ constructor(i: HalfPipe<I, O>, o: HalfPipe<O, I>) {
100
+ this.i = i;
101
+ this.o = o;
102
+ }
90
103
 
91
104
  pipes: Pipe<any, any>[] = [];
92
105
  registry: Record<PropertyKey, any> = {};
106
+ debug = false;
93
107
 
94
108
  clone(): this {
95
109
  const res = clone(this);
@@ -103,7 +117,7 @@ export class Pipe<I = any, O = any> {
103
117
  refine(
104
118
  valid: (data: O, ctx: Context) => boolean,
105
119
  name: string,
106
- props: Record<any, any>,
120
+ props: Record<any, any> = {},
107
121
  ): this {
108
122
  const res = this.clone();
109
123
  res.o.checks.push({ valid, name, props });
@@ -1,5 +1,6 @@
1
- import { expect, test } from "bun:test";
2
- import * as v from "./index";
1
+ import { test } from "node:test";
2
+ import expect from "expect";
3
+ import * as v from "./index.ts";
3
4
 
4
5
  test("number", () => {
5
6
  const schema = v.number();
@@ -12,7 +13,7 @@ test("number", () => {
12
13
  });
13
14
 
14
15
  test("custom validator", () => {
15
- const schema = v.number().refine((n) => n == 5, "eq", { n: 5 });
16
+ const schema = v.number().refine((n) => n === 5, "eq", { n: 5 });
16
17
 
17
18
  expect(schema.decode(5)).toEqual({ success: true, output: 5 });
18
19
  expect(schema.encode(3)).toEqual({
@@ -22,7 +23,7 @@ test("custom validator", () => {
22
23
  });
23
24
 
24
25
  test("custom context", () => {
25
- const schema = v.number().refine((n) => n == 5, "eq", { n: 5 });
26
+ const schema = v.number().refine((n) => n === 5, "eq", { n: 5 });
26
27
  class MyContext extends v.Context {
27
28
  errorFmt() {
28
29
  return "You done messed up";
package/src/primitives.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { HalfPipe, Pipe } from "./pipe";
1
+ import { HalfPipe, Pipe } from "./pipe.ts";
2
2
 
3
3
  function primitive<T>(name: string, typeCheck: (v: T) => v is T) {
4
4
  const half = new HalfPipe(name, typeCheck);
@@ -8,7 +8,7 @@ function primitive<T>(name: string, typeCheck: (v: T) => v is T) {
8
8
  export function boolean() {
9
9
  return primitive<boolean>(
10
10
  "boolean",
11
- (v): v is boolean => typeof v == "boolean",
11
+ (v): v is boolean => typeof v === "boolean",
12
12
  );
13
13
  }
14
14
 
@@ -16,12 +16,12 @@ export function boolean() {
16
16
  export function undefined() {
17
17
  return primitive<undefined>(
18
18
  "undefined",
19
- (v): v is undefined => typeof v == "undefined",
19
+ (v): v is undefined => typeof v === "undefined",
20
20
  );
21
21
  }
22
22
 
23
23
  export function any() {
24
- return primitive<any>("any", (v): v is any => true);
24
+ return primitive<any>("any", (_v): _v is any => true);
25
25
  }
26
26
 
27
27
  function null_() {
@@ -43,13 +43,13 @@ export class Comparable<I, O> extends Pipe<I, O> {
43
43
  return this.refine((v) => v <= n, "lte", { n });
44
44
  }
45
45
  eq(n: O) {
46
- return this.refine((v) => v == n, "eq", { n });
46
+ return this.refine((v) => v === n, "eq", { n });
47
47
  }
48
48
  }
49
49
 
50
50
  class ValioNumber extends Comparable<number, number> {
51
51
  constructor() {
52
- const half = new HalfPipe("number", (v) => typeof v == "number");
52
+ const half = new HalfPipe("number", (v) => typeof v === "number");
53
53
  super(half, half);
54
54
  }
55
55
  }
@@ -71,9 +71,9 @@ export class Arrayish<
71
71
  }
72
72
  }
73
73
 
74
- class ValioString extends Pipe<string, string> {
74
+ class ValioString extends Arrayish<string, string> {
75
75
  constructor() {
76
- const half = new HalfPipe("string", (v) => typeof v == "string");
76
+ const half = new HalfPipe("string", (v) => typeof v === "string");
77
77
  super(half, half);
78
78
  }
79
79
 
@@ -88,9 +88,12 @@ export function string(): ValioString {
88
88
  export type Lit = string | number | bigint | boolean | null | undefined;
89
89
 
90
90
  class ValioLiteral<T extends Lit> extends Pipe<T, T> {
91
- constructor(public literal: T) {
92
- const half = new HalfPipe(`${literal}`, (v): v is T => v == literal);
91
+ literal: T;
92
+
93
+ constructor(literal: T) {
94
+ const half = new HalfPipe(`${literal}`, (v): v is T => v === literal);
93
95
  super(half, half);
96
+ this.literal = literal;
94
97
  }
95
98
  }
96
99
  export function literal<T extends Lit>(literal: T) {
@@ -98,14 +101,17 @@ export function literal<T extends Lit>(literal: T) {
98
101
  }
99
102
 
100
103
  class ValioEnum<T extends Lit> extends Pipe<T, T> {
101
- constructor(public literals: T[]) {
104
+ literals: readonly T[];
105
+
106
+ constructor(literals: readonly T[]) {
102
107
  const half = new HalfPipe(`${literals.join(",")}`, (v: any): v is T =>
103
108
  literals.includes(v),
104
109
  );
105
110
  super(half, half);
111
+ this.literals = literals;
106
112
  }
107
113
  }
108
- function enum_<T extends Lit>(literals: T[]): ValioEnum<T> {
114
+ function enum_<T extends Lit>(literals: readonly T[]): ValioEnum<T> {
109
115
  return new ValioEnum(literals);
110
116
  }
111
117
  export { enum_ as enum };