inferred-types 0.25.0 → 0.26.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,268 @@
1
+ import { asArray } from "../lists/asArray";
2
+
3
+ // string filters
4
+ export type FilterStarts = {
5
+ /** one or more string which the value is allowed to start with */
6
+ startsWith: string | string[];
7
+ };
8
+ export type FilterIs = {
9
+ /** whether a string _**is**_ of a particular value */
10
+ is: string | string[];
11
+ };
12
+ export type FilterEnds = { endsWith: string | string[] };
13
+ export type FilterContains = {
14
+ /** whether any of the strings specified are _contained_ in the value */
15
+ contains: string | string[];
16
+ };
17
+
18
+ // numeric filters
19
+ export type FilterEquals = {
20
+ /** one or more values which _equal_ the value passed in */
21
+ equals: number | number[];
22
+ };
23
+ export type FilterNotEqual = {
24
+ /** one or more values which ALL _do not equal_ the value passed in */
25
+ notEqual: number | number[];
26
+ };
27
+ export type FilterGreaterThan = {
28
+ /** the incoming value is greater than this value */
29
+ greaterThan: number;
30
+ };
31
+ export type FilterLessThan = {
32
+ /** the incoming value is less than this value */
33
+ lessThan: number;
34
+ };
35
+
36
+ export type StringFilter =
37
+ | FilterIs
38
+ | FilterStarts
39
+ | FilterEnds
40
+ | FilterContains
41
+ | (FilterStarts & FilterEnds)
42
+ | (FilterStarts & FilterContains)
43
+ | (FilterEnds & FilterContains)
44
+ | (FilterStarts & FilterEnds & FilterContains);
45
+
46
+ export type NumericFilter =
47
+ | FilterEquals
48
+ | FilterNotEqual
49
+ | FilterGreaterThan
50
+ | FilterLessThan
51
+ | (FilterEquals & FilterNotEqual)
52
+ | (FilterEquals & FilterGreaterThan)
53
+ | (FilterEquals & FilterLessThan)
54
+ | (FilterNotEqual & FilterGreaterThan)
55
+ | (FilterNotEqual & FilterLessThan)
56
+ | (FilterGreaterThan & FilterLessThan)
57
+ | (FilterEquals & FilterGreaterThan & FilterLessThan)
58
+ | (FilterNotEqual & FilterGreaterThan & FilterLessThan)
59
+ | (FilterEquals & FilterNotEqual & FilterGreaterThan & FilterLessThan);
60
+
61
+ export type FilterDefn = StringFilter | NumericFilter;
62
+
63
+ // NOT filter
64
+ export type NotFilter = {
65
+ /**
66
+ * **not**
67
+ *
68
+ * If you want to build a filter who's conditions being met results in _filtering_
69
+ * the value rather than accepting it then choose this.
70
+ */
71
+ not: FilterDefn;
72
+ };
73
+
74
+ export function isNotFilter(f: FilterDefn | NotFilter): f is NotFilter {
75
+ return typeof f === "object" && "not" in f;
76
+ }
77
+
78
+ export type UnwrapNot<T extends FilterDefn | NotFilter> = T extends NotFilter
79
+ ? T["not"] extends StringFilter
80
+ ? StringFilter
81
+ : NumericFilter
82
+ : T;
83
+
84
+ export function isNumericFilter(filter: FilterDefn): filter is NumericFilter {
85
+ return "equals" in filter ||
86
+ "notEqual" in filter ||
87
+ "greaterThan" in filter ||
88
+ "lessThan" in filter
89
+ ? true
90
+ : false;
91
+ }
92
+
93
+ export type UndefinedValue<U extends boolean> = true extends U
94
+ ? "undefined treated as 'true'"
95
+ : U extends "no-impact"
96
+ ? "undefined treated in no-impact fashion"
97
+ : "undefined treated as 'false'";
98
+
99
+ export type UndefinedTreatment = "undefined treated as 'true'" | "undefined treated as 'false'";
100
+
101
+ export type LogicalCombinator = "AND" | "OR";
102
+
103
+ /**
104
+ * **FilterFn**
105
+ *
106
+ * A filter function derived from the `filter()` configurator. This function is intended to provide a type-strong _filter_ function which can be used like so:
107
+ * be used like:
108
+ * ```ts
109
+ * const onlyPrivate = filter({ startsWith: "_" });
110
+ * const privateFiles = files.filter(onlyPrivate);
111
+ * ```
112
+ *
113
+ */
114
+ export type FilterFn<T extends StringFilter | NumericFilter> = T extends StringFilter
115
+ ? <V extends string | undefined>(input: V) => boolean
116
+ : <V extends number | undefined>(input: V) => boolean;
117
+
118
+ /**
119
+ * Defines a logical function for each condition type
120
+ */
121
+ export type ConditionFilter<T extends StringFilter | NumericFilter> = T extends StringFilter
122
+ ? (input: string) => boolean
123
+ : (input: number) => boolean;
124
+
125
+ /**
126
+ * Wraps numeric filter's configuration into a logic filter
127
+ */
128
+ const numericOps = <L extends LogicalCombinator>(config: NumericFilter, boolLogic: L) => {
129
+ const equals = (n: number | number[]): [ConditionFilter<NumericFilter>] | [] => {
130
+ const f = asArray(n);
131
+ return f.length === 0 ? [] : [(input?: number) => f.some((i) => i === input)];
132
+ };
133
+ const notEqual = (n: number | number[]): [ConditionFilter<NumericFilter>] | [] => {
134
+ const f = asArray(n);
135
+ return f.length === 0 ? [] : [(input?: number) => f.every((i) => i !== input)];
136
+ };
137
+
138
+ /** returns 1 FilterFn's */
139
+ const greaterThan = <V extends number>(n: V) => {
140
+ const val = [(input?: V) => input !== undefined && input > n] as [(input: V) => boolean];
141
+ return val as [ConditionFilter<NumericFilter>];
142
+ };
143
+
144
+ /** returns 0 or 1 FilterFn's */
145
+ const lessThan = <V extends number>(n: V) => {
146
+ const val = [(input: V) => input !== undefined && input < n];
147
+ return val as [ConditionFilter<NumericFilter>] | [];
148
+ };
149
+
150
+ const conditions = [
151
+ //
152
+ ...("equals" in config ? equals(config.equals) : []),
153
+ ...("notEqual" in config ? notEqual(config.notEqual) : []),
154
+ ...("greaterThan" in config ? greaterThan(config.greaterThan) : []),
155
+ ...("lessThan" in config ? lessThan(config.lessThan) : []),
156
+ ];
157
+
158
+ const combined =
159
+ boolLogic === "AND"
160
+ ? <N extends number>(input: N) => conditions.every((c) => c(input))
161
+ : <N extends number>(input: N) => conditions.some((c) => c(input));
162
+
163
+ return combined;
164
+ };
165
+
166
+ /** wraps configuration along with boolean logic */
167
+ const stringOps = <L extends LogicalCombinator>(
168
+ config: StringFilter,
169
+ boolLogic: L
170
+ ): ConditionFilter<StringFilter> => {
171
+ const startsWith = (n: string | string[]): [ConditionFilter<StringFilter>] | [] => {
172
+ const f = asArray(n);
173
+ return f.length === 0 ? [] : [(input?: string) => f.some((i) => input?.startsWith(i))];
174
+ };
175
+ const endsWith = (n: string | string[]): [ConditionFilter<StringFilter>] | [] => {
176
+ const f = asArray(n);
177
+ return f.length === 0 ? [] : [(input?: string) => f.some((i) => input?.endsWith(i))];
178
+ };
179
+ const contains = (n: string | string[]): [ConditionFilter<StringFilter>] | [] => {
180
+ const f = asArray(n);
181
+ return f.length === 0 ? [] : [(input: string) => f.some((i) => input?.includes(i))];
182
+ };
183
+
184
+ const conditions = [
185
+ ...("startsWith" in config ? startsWith(config.startsWith) : []),
186
+ ...("endsWith" in config ? endsWith(config.endsWith) : []),
187
+ ...("contains" in config ? contains(config.contains) : []),
188
+ ];
189
+
190
+ const combined =
191
+ boolLogic === "AND"
192
+ ? (input: string) => conditions.every((c) => c(input))
193
+ : (input: string) => conditions.some((c) => c(input));
194
+
195
+ return combined as ConditionFilter<StringFilter>;
196
+ };
197
+
198
+ const filterFn = <
199
+ F extends FilterDefn | NotFilter,
200
+ C extends LogicalCombinator,
201
+ U extends boolean | "no-impact"
202
+ >(
203
+ defn: F,
204
+ logicCombinator: C,
205
+ ifUndefined: U = "no-impact" as U
206
+ ): FilterFn<UnwrapNot<F>> => {
207
+ type D = UnwrapNot<F>;
208
+ const config: D = isNotFilter(defn) ? (defn["not"] as D) : (defn as D);
209
+
210
+ const filter: FilterFn<D> = (
211
+ //
212
+ input: Parameters<FilterFn<D>>[0]
213
+ ) => {
214
+ const undefValue =
215
+ ifUndefined === "no-impact"
216
+ ? logicCombinator === "AND"
217
+ ? true
218
+ : false
219
+ : (ifUndefined as true | false);
220
+
221
+ let flag: boolean;
222
+
223
+ if (typeof input === "undefined") {
224
+ flag = undefValue;
225
+ } else if (isNumericFilter(config)) {
226
+ const fn = numericOps(config, logicCombinator);
227
+ flag = isNotFilter(defn) ? !fn(input as number) : fn(input as number);
228
+ } else {
229
+ const fn = stringOps(config, logicCombinator);
230
+ flag = isNotFilter(defn) ? !fn(input as string) : fn(input as string);
231
+ }
232
+
233
+ return flag;
234
+ };
235
+
236
+ return filter;
237
+ };
238
+
239
+ /**
240
+ * **filter**
241
+ *
242
+ * A higher order helper utility which builds a boolean _filter_ function based on a simple
243
+ * configuration object. Support either _string_ or _numeric_ filters.
244
+ *
245
+ * ```ts
246
+ * const str = filter({startsWith: ["_", "."], endsWith: ".md"});
247
+ * const num = filter({ greaterThan: 55, notEqual: [66, 77]});
248
+ * ```
249
+ *
250
+ * All conditions (e.g., `startsWith`, `contains`, `notEqual`, etc.) that a particular filter
251
+ * is defined as having -- if there is more than one -- will be logically combined using AND
252
+ * unless specified otherwise in the third parameter.
253
+ *
254
+ * How a value of _undefined_ will be treated is stated in the second parameter but defaults
255
+ * to "no-impact" which means it's `false` when the logicCombinator is OR but defaults
256
+ * to `true` when the logicCombinator is AND.
257
+ */
258
+ export const filter = <
259
+ F extends FilterDefn | NotFilter,
260
+ U extends boolean | "no-impact",
261
+ C extends LogicalCombinator
262
+ >(
263
+ config: F,
264
+ logicCombinator: C = "AND" as C,
265
+ ifUndefined: U = "no-impact" as U
266
+ ) => {
267
+ return filterFn(config, logicCombinator, ifUndefined);
268
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./and";
2
+ export * from "./or";
3
+ export * from "./not";
4
+ export * from "./filter";
@@ -0,0 +1,15 @@
1
+ import { LogicFunction } from "src/types/functions";
2
+
3
+ /**
4
+ * Groups a number of "logic functions" together by combining their results using
5
+ * the logical **NOT** operator.
6
+ *
7
+ * **Note:** a "logic function" is any function which returns a boolean
8
+ */
9
+ export const not = <T extends any[]>(op: LogicFunction<T>): LogicFunction<T> => {
10
+ const fn: LogicFunction<T> = (...args: T) => {
11
+ return !op(...args);
12
+ };
13
+
14
+ return fn;
15
+ };
@@ -0,0 +1,15 @@
1
+ import { LogicFunction } from "src/types/functions";
2
+
3
+ /**
4
+ * Groups a number of "logic functions" together by combining their results using
5
+ * the logical **OR** operator.
6
+ *
7
+ * **Note:** a "logic function" is any function which returns a boolean
8
+ */
9
+ export const or = <T extends any[]>(...ops: LogicFunction<T>[]): LogicFunction<T> => {
10
+ const fn: LogicFunction<T> = (...args: T) => {
11
+ return [...ops].some((i) => i(...args));
12
+ };
13
+
14
+ return fn;
15
+ };
@@ -10,11 +10,11 @@ export * from "./keys";
10
10
  export * from "./ruleset";
11
11
  // directory exports
12
12
  export * from "./api/index";
13
+ export * from "./boolean-logic/index";
13
14
  export * from "./dictionary/index";
14
15
  export * from "./errors/index";
15
16
  export * from "./lists/index";
16
17
  export * from "./literals/index";
17
- export * from "./map-reduce/index";
18
18
  export * from "./modelling/index";
19
19
  export * from "./runtime/index";
20
20
  export * from "./state/index";
@@ -0,0 +1,34 @@
1
+ import { Narrowable, Widen } from "src/types";
2
+ import { isArray } from "../runtime";
3
+
4
+ /**
5
+ * Type utility which converts `undefined[]` to `unknown[]`
6
+ */
7
+ export type UndefinedArrayIsUnknown<T extends any[]> = undefined[] extends T ? unknown[] : T;
8
+
9
+ export type AsArray<T, W extends boolean = false> = T extends any[]
10
+ ? W extends true
11
+ ? Widen<T>
12
+ : T
13
+ : W extends true
14
+ ? UndefinedArrayIsUnknown<Widen<T>[]>
15
+ : UndefinedArrayIsUnknown<T[]>;
16
+
17
+ /**
18
+ * Ensures that any input passed in is passed back as an array:
19
+ *
20
+ * - if it was already an array than this just serves as an _identity_ function
21
+ * - if it was not then it wraps the element into a one element array of the
22
+ * given type
23
+ *
24
+ * Note: by default the _type_ of values will be intentionally widened so that the value "abc"
25
+ * is of type `string` not the literal `abc`. If you want to keep literal types then
26
+ * change the optional _widen_ parameter to _false_.
27
+ */
28
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
29
+ export const asArray = <T extends Narrowable, W extends boolean = true>(thing: T, _widen?: W) => {
30
+ return (isArray(thing) ? thing : typeof thing === "undefined" ? ([] as T[]) : [thing]) as AsArray<
31
+ T,
32
+ W
33
+ >;
34
+ };
@@ -5,6 +5,7 @@
5
5
  // hash-code: bf4a6791
6
6
 
7
7
  // file exports
8
+ export * from "./asArray";
8
9
  export * from "./groupBy";
9
10
 
10
11
  // #endregion auto-indexed files
@@ -0,0 +1,21 @@
1
+ import { describe, it, expect } from "vitest";
2
+ // import type { Equal, Expect } from "@type-challenges/utils";
3
+ import { and, filter } from "src/utility";
4
+
5
+ describe("boolean logic", () => {
6
+ it("AND operation", () => {
7
+ const a1 = and(
8
+ () => true,
9
+ () => false
10
+ );
11
+ const a2 = and(
12
+ () => true,
13
+ () => true
14
+ );
15
+ const md = and(filter({ endsWith: ".md" }), filter({ not: { startsWith: "_" } }));
16
+
17
+ expect(a1()).toBe(false);
18
+ expect(a2()).toBe(true);
19
+ expect(["foo.md", "bar.html", "_baz.md"].filter(md)).toEqual(["foo.md"]);
20
+ });
21
+ });
@@ -0,0 +1,52 @@
1
+ import { filter } from "src/utility";
2
+ import { Equal, Expect } from "@type-challenges/utils";
3
+ import { describe, it, expect } from "vitest";
4
+
5
+ describe("filter() utility function", () => {
6
+ it("string filter built and gives proper types", () => {
7
+ const f = filter({ startsWith: "th" });
8
+ type P = Parameters<typeof f>;
9
+ type R = ReturnType<typeof f>;
10
+ const notPrivate = filter({ not: { startsWith: ["_", "."] } });
11
+
12
+ // runtime
13
+ expect(typeof f).toEqual("function");
14
+ expect(["one", "two", "three"].filter(f)).toEqual(["three"]);
15
+ expect(["foo.md", "bar.html", "_private.md", ".bobs-your-uncle.txt"].filter(notPrivate)) //
16
+ .toEqual(["foo.md", "bar.html"]);
17
+
18
+ // design time
19
+ type cases = [
20
+ Expect<Equal<P[0], string | undefined>>, //
21
+ Expect<Equal<R, boolean>>
22
+ ];
23
+ const cases: cases = [true, true];
24
+ });
25
+
26
+ it("numeric filter built and gives proper types", () => {
27
+ const f = filter({ equals: 42 });
28
+ type P = Parameters<typeof f>;
29
+ type R = ReturnType<typeof f>;
30
+
31
+ // runtime
32
+ expect(typeof f).toEqual("function");
33
+ expect([1, 2, 3, 4, 5].filter(filter({ greaterThan: 3 }))) //
34
+ .toEqual([4, 5]);
35
+ expect([1, 2, 3, 4, 5].filter(filter({ not: { greaterThan: 3 } }))) //
36
+ .toEqual([1, 2, 3]);
37
+
38
+ // design time
39
+ type cases = [
40
+ Expect<Equal<P[0], number | undefined>>, //
41
+ Expect<Equal<R, boolean>>
42
+ ];
43
+ const cases: cases = [true, true];
44
+ });
45
+
46
+ it("string filter's startsWith", () => {
47
+ const f = filter({ startsWith: "." });
48
+ const remaining = ["foo", "bar", ".baz"].filter(f);
49
+
50
+ expect(remaining).toEqual([".baz"]);
51
+ });
52
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { asArray } from "src/utility";
3
+ import { Equal, Expect } from "@type-challenges/utils";
4
+
5
+ describe("asArray() function", () => {
6
+ it("non-array is returned as an array", () => {
7
+ const i = "a";
8
+ const o = asArray(i);
9
+ type O = typeof o;
10
+
11
+ // run-time
12
+ expect(o).toEqual(["a"]);
13
+ // design-time
14
+ type cases = [Expect<Equal<O, string[]>>];
15
+ const cases: cases = [true];
16
+ });
17
+
18
+ it("array is returned as an array", () => {
19
+ const i = ["a"];
20
+ const o = asArray(i);
21
+ type O = typeof o;
22
+
23
+ // run-time
24
+ expect(o).toEqual(["a"]);
25
+ // design-time
26
+ type cases = [Expect<Equal<O, string[]>>];
27
+ const cases: cases = [true];
28
+ });
29
+
30
+ it("non-array literal is returned as an array", () => {
31
+ const i = "a" as const;
32
+ const o = asArray(i, false);
33
+ const o2 = asArray(i, true);
34
+
35
+ type O = typeof o;
36
+ type O2 = typeof o2;
37
+
38
+ // run-time
39
+ expect(o).toEqual(["a"]);
40
+ // design-time
41
+ type cases = [
42
+ Expect<Equal<O, "a"[]>>, //
43
+ Expect<Equal<O2, string[]>>
44
+ ];
45
+ const cases: cases = [true, true];
46
+ });
47
+
48
+ it("handling non-array element which presents as undefined", () => {
49
+ type T = string | undefined;
50
+ const i = undefined;
51
+ const i2: T = undefined;
52
+ const o = asArray(i);
53
+ const o2 = asArray(i2 as T);
54
+ const o3 = asArray(i2 as T, false);
55
+ type O = typeof o;
56
+ type O2 = typeof o2;
57
+ type O3 = typeof o3;
58
+
59
+ // run-time
60
+ expect(o).toEqual([]);
61
+ expect(o2).toEqual([]);
62
+ // design-time
63
+ type cases = [
64
+ Expect<Equal<O, unknown[]>>, //
65
+ // TODO: would be nice to extract the unknown[] part of the union
66
+ Expect<Equal<O2, unknown[] | string[]>>,
67
+ Expect<Equal<O3, unknown[] | string[]>>
68
+ ];
69
+ const cases: cases = [true, true, true];
70
+ });
71
+
72
+ it("handling array element which contains undefined is unaffected", () => {
73
+ type T = string | undefined;
74
+ const i = [undefined, "foobar"];
75
+ const i2: T[] = [undefined, "foobar"];
76
+ const o = asArray(i);
77
+ const o2 = asArray(i2 as T[]);
78
+ type O = typeof o;
79
+ type O2 = typeof o2;
80
+
81
+ // run-time
82
+ expect(o).toEqual([undefined, "foobar"]);
83
+ expect(o2).toEqual([undefined, "foobar"]);
84
+ // design-time
85
+ type cases = [
86
+ Expect<Equal<O, (string | undefined)[]>>, //
87
+ Expect<Equal<O2, T[]>>
88
+ ];
89
+ const cases: cases = [true, true];
90
+ });
91
+ });
@@ -1,35 +0,0 @@
1
- // inspired by [article](https://medium.com/@reidev275/creating-a-type-safe-dsl-for-filtering-in-typescript-53fe68a7942e)]
2
-
3
-
4
- export type Filter<A> =
5
- | { kind: "Equals"; field: keyof A; val: A[keyof A] }
6
- | { kind: "Greater"; field: keyof A; val: A[keyof A] }
7
- | { kind: "Less"; field: keyof A; val: A[keyof A] }
8
- | { kind: "And"; a: Filter<A>; b: Filter<A> }
9
- | { kind: "Or"; a: Filter<A>; b: Filter<A> };
10
-
11
- export const equals = <A, K extends keyof A>(field: K, val: A[K]): Filter<A> => ({
12
- kind: "Equals",
13
- field,
14
- val
15
- });
16
- export const greater = <A, K extends keyof A>(field: K, val: A[K]): Filter<A> => ({
17
- kind: "Greater",
18
- field,
19
- val
20
- });
21
- export const less = <A, K extends keyof A>(field: K, val: A[K]): Filter<A> => ({
22
- kind: "Less",
23
- field,
24
- val
25
- });
26
- export const and = <A>(a: Filter<A>, b: Filter<A>): Filter<A> => ({
27
- kind: "And",
28
- a,
29
- b
30
- });
31
- export const or = <A>(a: Filter<A>, b: Filter<A>): Filter<A> => ({
32
- kind: "Or",
33
- a,
34
- b
35
- });
@@ -1,12 +0,0 @@
1
- // #autoindex
2
- // #region auto-indexed files
3
- // index last changed at: 8th Aug, 2022, 09:51 AM ( GMT-7 )
4
- // hash-code: 5ffc538b
5
-
6
- // file exports
7
- export * from "./filter";
8
-
9
- // #endregion auto-indexed files
10
-
11
- // see https://github.com/inocan-group/do-devops/docs/autoindex.md
12
- // for more info