diff --git a/README.md b/README.md
index f05cccb..538ed49 100644
--- a/README.md
+++ b/README.md
@@ -2,98 +2,211 @@
![typescript](https://img.shields.io/badge/written%20for-typescript-3178c6?style=flat-square) [![codecov](https://img.shields.io/codecov/c/github/adrgautier/soit?style=flat-square&token=IPTGBDRRJE)](https://codecov.io/gh/adrgautier/soit) ![prettier](https://img.shields.io/badge/code%20style-prettier-ff69b4?style=flat-square) [![npm](https://img.shields.io/npm/v/soit?style=flat-square)](https://www.npmjs.com/package/soit)
-**Soit** (French for: either) is like an enhanced [Set()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) function which simplifies **type narrowing** and aims to replace TypeScript enums.
+**Soit** (French for: either) is like an enhanced [Set()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) function which provides **type narrowing** and **template** `beta` utils.
## Motivation
-The `enum` feature of TypeScript is not ideal. It does not provide type guards and is not iterable.
+One of the main goal of TypeScript is to deal with **uncertainty**, to ensure that all possibilities have been taken into account during compilation. Sometimes the type itself can be uncertain (e.g. is it a `string` or a `number`?), but it is also common to know all possible values before runtime.
-I wanted a simple lib which provides a way to [narrow type](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to a given set of values and can be iterated.
+The simplest way to declare all possible values is to write a union:
+```ts
+type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
+```
-> Inspired from the [enum feature of zod](https://github.com/colinhacks/zod/tree/v1#zod-enums).
+Another approach is to use the enum feature of TypeScript:
+```ts
+enum HTTPMethod {
+ GET = "GET",
+ POST = "POST",
+ PUT = "PUT",
+ DELETE = "DELETE"
+}
+```
+
+Whatever approach you use, you won't be able (easily) to [narrow a type down](https://www.typescriptlang.org/docs/handbook/2/narrowing.html) to this set of values or to [get an array from](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) this set of values.
+
+I created *Soit* to be a simple lib that provide the features aforementioned.
## Declaration
-A *Soit* instance can be created by passing literals (string, number or boolean) in an array to the `Soit` function.
+A *Soit* instance can be created by passing an array of *literals* to the `Soit()` function.
```ts
-const isWarmColor = Soit(["red", "orange"]);
-```
+import Soit from "soit";
-You can infer the corresponding union using the `Infer` "helper" provided by the lib.
-```ts
-type WarmColor = Infer; // infers "red" | "orange"
+const isHTTPMethod = Soit(["GET", "POST", "PUT", "DELETE"]);
```
-You can pass any string, number or boolean you want.
+A [literal](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) is a specific string, number or boolean.
```ts
-const isColdColor = Soit(["one", 1, true]);
+const isOne = Soit(["1", "one", 1, true]);
```
-*Soit* instances are **iterable** and can be used to create new definitions.
+You can infer the corresponding type using the `Infer` generic provided by the lib.
```ts
-const isColor = Soit([...isWarmColor, "green", "blue"]);
+import { Infer } from "soit";
-type Color = Infer; // infers "red" | "orange" | "green" | "blue"
+type HTTPMethod = Infer; // infers "GET" | "POST" | "PUT" | "DELETE"
```
-## Guard
+## Guard behavior
A *Soit* instance is intended to be used as a type guard:
```ts
-function handleColor(color: Color) {
- if(isWarmColor(color)) {
- // color can be "red" | "orange"
+function handleHTTPMethod(method: string) {
+ if(isHTTPMethod(method)) {
+ // method's value is "GET", "POST", "PUT" or "DELETE"
}
- // color can be "blue" | "green"
+ throw new Error("Unknown HTTP method.");
}
```
-## Array utils
+## Iterable behavior
Because the *Soit* instance is **iterable**, you can access the corresponding array:
```ts
-const colors = Array.from(isColor);
+const HTTPMethodArray = Array.from(isHTTPMethod);
```
You may prefer this syntax:
```ts
-const colors = [...isColor];
+const HTTPMethodArray = [...isHTTPMethod];
```
-`map` and `forEach` can be used without `Array.from()`.
+## Array methods `deprecated`
+
+A *Soit* instance gives access to two Array methods : `map` and `forEach`
+
```ts
-isColor.forEach((color) => console.log(color));
+isHTTPMethod.forEach(method => console.log(method));
-const uppercaseColors = isColor.map(color => color.toUpperCase());
+const lowerCaseHTTPMethodArray = isHTTPMethod.map(method => method.toLowerCase());
```
-## Set utils
+> `map` and `forEach` are simple shortcuts, e.g. :
+> ```ts
+> [...isHTTPMethod].forEach(method => console.log(method));
+> ```
+>
+> The map method for instance can be confusing because it does not return a new *Soit* instance.
+> For this reason, both methods will be removed with the next major release.
+
+## Set methods
+
+Set methods aim to create new *Soit* instances by adding or subtracting values from an existing instance.
+
+> I created these methods before the new [composition methods](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set#set_composition) where added to the **Set** object.
+> This new API will certainly influence the naming of *Soit* methods in the next major release.
### `.subset([])`
You can create subsets using the `subset` method.
```ts
-const isWarmColor = isColor.subset(["red", "orange"]);
+const isHTTPMutationMethod = isHTTPMethod.subset(["POST", "PUT", "DELETE"]);
```
-> This checks on build time that `"red"` and `"orange"` do exist in the `isColor` instance.
+> This checks on build time that `"POST"`, `"PUT"` and `"DELETE"` do exist in the `isHTTPMethod` instance.
### `.extend([])`
You can extend an existing *Soit* instance using the `extend` method.
```ts
-const isColor = isWarmColor.extend(["blue", "green"]);
+const isHTTPMethod = isHTTPMutationMethod.extend(["GET"]);
```
### `.difference([])`
+
You can create a new instance without the specified values using the `difference` method.
```ts
-const isColdColor = isColor.difference(["red", "orange", "yellow"]);
+const isHTTPQueryMethod = isHTTPMethod.difference(["POST", "PUT", "DELETE"]);
```
> The given array don't need to be a subset and can contain values that don't exist in the initial instance.
+## Template `beta`
+
+The template feature allows mimicking the [template literal type](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#handbook-content) mechanism, but with runtime utils.
+Let's take the following template literal type:
+
+```ts
+type TimeGetter = `get${"Seconds" | "Minutes" | "Hours"}`;
+```
+
+The `TimeGetter` type will only accept the following values: `"getSeconds"`, `"getMinutes"` and `"getHours"`.
+
+Here is how you would use the template feature from *Soit*:
+
+```ts
+const isUnit = Soit(["Seconds", "Minutes", "Hours"]);
+const isTimeGetter = Soit.Template("get", isUnit);
+```
+The `Template()` function is able to construct the corresponding template using the strings as the "static" parts and the *Soit* instances as the "dynamic" parts.
+
+You can get the corresponding type with the usual `Infer` generic.
+
+```ts
+type TimeGetter = Infer;
+```
+
+### Guard behavior
+
+Like a *Soit* instance, a *SoitTemplate* is intended to be used as a type guard:
+```ts
+if(isTimeGetter(method)) { ... }
+```
+The `isTimeGetter` guard will only accept the following values: `"getSeconds"`, `"getMinutes"` and `"getHours"`.
+
+### Capture method 🪄
+
+A *SoitTemplate* instance offers the `capture` method to retrieve the "dynamic" parts of the template from a string.
+
+```ts
+const [unit] = isTimeGetter.capture("getSeconds"); // unit === "Seconds"
+```
+
+### Iterable behavior and Array method
+
+A *SoitTemplate* instance is iterable.
+
+```ts
+const timeGetterMethods = [...isTimeGetter]; // ["getSeconds", "getMinutes", "getHours"]
+```
+
+As with a regular *Soit* instance, you get the `map` and `forEach` shortcuts.
+
+## Using *Soit* with other tools
+
+### [TS-Pattern](https://github.com/gvergnaud/ts-pattern)
+
+You can easily integrate *Soit* instances to your patterns using the [P.when](https://github.com/gvergnaud/ts-pattern?tab=readme-ov-file#pwhen-patterns) util :
+
+```ts
+import { P } from "ts-pattern";
+
+const pattern = P.when(isHTTPMethod);
+```
+
+The inference will work as expected with TS-Pattern logic :
+
+```ts
+type Pattern = P.infer; // infers "GET" | "POST" | "PUT" | "DELETE"
+```
+
+### [Zod](https://github.com/colinhacks/zod)
+
+You can integrate *Soit* instances to your Zod schemas using the [custom](https://zod.dev/?id=custom-schemas) util:
+
+```ts
+import * as z from "zod";
+import { Infer } from "soit";
+
+type HTTPMethod = Infer;
+
+const HTTPMethodSchema = z.custom(isHTTPMethod);
+```
+
+Zod is not able to infer the type on its own, therefore you need to pass the corresponding type (inferred beforehand) in the generic.
+
## Troubleshoot
### `Type 'string' is not assignable to type 'never'.` ts(2345)
@@ -101,13 +214,24 @@ const isColdColor = isColor.difference(["red", "orange", "yellow"]);
You are maybe trying to create a new *Soit* instance using a named array.
```ts
-const warmColors = ["red", "orange"];
-const isWarmColor = Soit(warmColors); // error ts(2345)
+const HTTPMutationMethods = ["POST", "PUT", "DELETE"];
+const isHTTPMutationMethods = Soit(HTTPMutationMethods); // error ts(2345)
```
*Soit* throw this error to prevent passing an unknown set of value (i.e. `string[]`). The solution here is to use the `as const` declaration in order to freeze the values and allow a proper type inference.
```ts
-const warmColors = ["red", "orange"] as const;
-const isWarmColor = Soit(warmColors);
+const HTTPMutationMethods = ["POST", "PUT", "DELETE"] as const;
+const isHTTPMutationMethods = Soit(HTTPMutationMethods);
+```
+
+The `Template()` function also requires freezing the values to allow a proper type inference :
+```ts
+const template = ['get', Soit(['Seconds', 'Minutes', 'Hours'])] as const;
+const isTimeGetter = Soit.Template(...template);
```
+
+This error can also occur if you pass a *Soit* instance directly to a template. You can use `as const` as follows:
+```ts
+const isTimeGetter = Soit.Template('get', Soit(['Seconds', 'Minutes', 'Hours'] as const));
+```
\ No newline at end of file
diff --git a/package.json b/package.json
index faeb76d..53fc77b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "soit",
- "version": "2.0.0",
+ "version": "2.1.0",
"description": "Create a type guard from a list of literals.",
"main": "dist/index.js",
"sideEffects": false,
@@ -19,7 +19,7 @@
"build": "npm run clean && tsc",
"test": "jest --coverage && npm run compile",
"compile": "tsc -p ./tsconfig.test.json --noEmit",
- "prettier": "prettier '(src|tests)/**/*.ts' --write"
+ "prettier": "prettier 'src/**/*.ts' --write"
},
"repository": {
"type": "git",
@@ -36,21 +36,20 @@
"license": "MIT",
"jest": {
"transform": {
- "^.+\\.tsx?$": "ts-jest"
+ "^.+\\.ts$": "ts-jest"
},
- "testRegex": "/tests/.*.test.ts$",
+ "testRegex": "/src/.*.test.ts$",
"testPathIgnorePatterns": [
"/node_modules/",
"/dist/"
],
"collectCoverageFrom": [
- "src/**/*.ts"
+ "src/**/*.ts",
+ "!src/**/*types.ts"
],
"moduleFileExtensions": [
"ts",
- "tsx",
"js",
- "jsx",
"json",
"node"
]
diff --git a/prettier.config.js b/prettier.config.js
index 09a6af1..38a9e85 100644
--- a/prettier.config.js
+++ b/prettier.config.js
@@ -1,24 +1,10 @@
module.exports = {
- // write: true,
semi: true,
useTabs: false, // Indent lines with tabs instead of spaces.
printWidth: 80, // Specify the length of line that the printer will wrap on.
tabWidth: 2, // Specify the number of spaces per indentation-level.
singleQuote: true, // Use single quotes instead of double quotes.
- /**
- * Print trailing commas wherever possible.
- * Valid options:
- * - "none" - no trailing commas
- * - "es5" - trailing commas where valid in ES5 (objects, arrays, etc)
- * - "all" - trailing commas wherever possible (function arguments)
- */
- trailingComma: 'es5',
- /**
- * Specify which parse to use.
- * Valid options:
- * - "flow"
- * - "babylon"
- */
+ trailingComma: 'es5', // Print trailing commas wherever possible.
parser: 'typescript',
arrowParens: 'avoid',
};
diff --git a/src/__tests__/__snapshots__/template.test.ts.snap b/src/__tests__/__snapshots__/template.test.ts.snap
new file mode 100644
index 0000000..fbd8ef5
--- /dev/null
+++ b/src/__tests__/__snapshots__/template.test.ts.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SoitTemplate should test a complex template 1`] = `"0-0-0|0-0-1|0-0-2|0-0-3|0-0-4|0-0-5|0-0-6|0-0-7|0-0-8|0-0-9|0-1-0|0-1-1|0-1-2|0-1-3|0-1-4|0-1-5|0-1-6|0-1-7|0-1-8|0-1-9|0-2-0|0-2-1|0-2-2|0-2-3|0-2-4|0-2-5|0-2-6|0-2-7|0-2-8|0-2-9|0-3-0|0-3-1|0-3-2|0-3-3|0-3-4|0-3-5|0-3-6|0-3-7|0-3-8|0-3-9|0-4-0|0-4-1|0-4-2|0-4-3|0-4-4|0-4-5|0-4-6|0-4-7|0-4-8|0-4-9|0-5-0|0-5-1|0-5-2|0-5-3|0-5-4|0-5-5|0-5-6|0-5-7|0-5-8|0-5-9|0-6-0|0-6-1|0-6-2|0-6-3|0-6-4|0-6-5|0-6-6|0-6-7|0-6-8|0-6-9|0-7-0|0-7-1|0-7-2|0-7-3|0-7-4|0-7-5|0-7-6|0-7-7|0-7-8|0-7-9|0-8-0|0-8-1|0-8-2|0-8-3|0-8-4|0-8-5|0-8-6|0-8-7|0-8-8|0-8-9|0-9-0|0-9-1|0-9-2|0-9-3|0-9-4|0-9-5|0-9-6|0-9-7|0-9-8|0-9-9|1-0-0|1-0-1|1-0-2|1-0-3|1-0-4|1-0-5|1-0-6|1-0-7|1-0-8|1-0-9|1-1-0|1-1-1|1-1-2|1-1-3|1-1-4|1-1-5|1-1-6|1-1-7|1-1-8|1-1-9|1-2-0|1-2-1|1-2-2|1-2-3|1-2-4|1-2-5|1-2-6|1-2-7|1-2-8|1-2-9|1-3-0|1-3-1|1-3-2|1-3-3|1-3-4|1-3-5|1-3-6|1-3-7|1-3-8|1-3-9|1-4-0|1-4-1|1-4-2|1-4-3|1-4-4|1-4-5|1-4-6|1-4-7|1-4-8|1-4-9|1-5-0|1-5-1|1-5-2|1-5-3|1-5-4|1-5-5|1-5-6|1-5-7|1-5-8|1-5-9|1-6-0|1-6-1|1-6-2|1-6-3|1-6-4|1-6-5|1-6-6|1-6-7|1-6-8|1-6-9|1-7-0|1-7-1|1-7-2|1-7-3|1-7-4|1-7-5|1-7-6|1-7-7|1-7-8|1-7-9|1-8-0|1-8-1|1-8-2|1-8-3|1-8-4|1-8-5|1-8-6|1-8-7|1-8-8|1-8-9|1-9-0|1-9-1|1-9-2|1-9-3|1-9-4|1-9-5|1-9-6|1-9-7|1-9-8|1-9-9|2-0-0|2-0-1|2-0-2|2-0-3|2-0-4|2-0-5|2-0-6|2-0-7|2-0-8|2-0-9|2-1-0|2-1-1|2-1-2|2-1-3|2-1-4|2-1-5|2-1-6|2-1-7|2-1-8|2-1-9|2-2-0|2-2-1|2-2-2|2-2-3|2-2-4|2-2-5|2-2-6|2-2-7|2-2-8|2-2-9|2-3-0|2-3-1|2-3-2|2-3-3|2-3-4|2-3-5|2-3-6|2-3-7|2-3-8|2-3-9|2-4-0|2-4-1|2-4-2|2-4-3|2-4-4|2-4-5|2-4-6|2-4-7|2-4-8|2-4-9|2-5-0|2-5-1|2-5-2|2-5-3|2-5-4|2-5-5|2-5-6|2-5-7|2-5-8|2-5-9|2-6-0|2-6-1|2-6-2|2-6-3|2-6-4|2-6-5|2-6-6|2-6-7|2-6-8|2-6-9|2-7-0|2-7-1|2-7-2|2-7-3|2-7-4|2-7-5|2-7-6|2-7-7|2-7-8|2-7-9|2-8-0|2-8-1|2-8-2|2-8-3|2-8-4|2-8-5|2-8-6|2-8-7|2-8-8|2-8-9|2-9-0|2-9-1|2-9-2|2-9-3|2-9-4|2-9-5|2-9-6|2-9-7|2-9-8|2-9-9|3-0-0|3-0-1|3-0-2|3-0-3|3-0-4|3-0-5|3-0-6|3-0-7|3-0-8|3-0-9|3-1-0|3-1-1|3-1-2|3-1-3|3-1-4|3-1-5|3-1-6|3-1-7|3-1-8|3-1-9|3-2-0|3-2-1|3-2-2|3-2-3|3-2-4|3-2-5|3-2-6|3-2-7|3-2-8|3-2-9|3-3-0|3-3-1|3-3-2|3-3-3|3-3-4|3-3-5|3-3-6|3-3-7|3-3-8|3-3-9|3-4-0|3-4-1|3-4-2|3-4-3|3-4-4|3-4-5|3-4-6|3-4-7|3-4-8|3-4-9|3-5-0|3-5-1|3-5-2|3-5-3|3-5-4|3-5-5|3-5-6|3-5-7|3-5-8|3-5-9|3-6-0|3-6-1|3-6-2|3-6-3|3-6-4|3-6-5|3-6-6|3-6-7|3-6-8|3-6-9|3-7-0|3-7-1|3-7-2|3-7-3|3-7-4|3-7-5|3-7-6|3-7-7|3-7-8|3-7-9|3-8-0|3-8-1|3-8-2|3-8-3|3-8-4|3-8-5|3-8-6|3-8-7|3-8-8|3-8-9|3-9-0|3-9-1|3-9-2|3-9-3|3-9-4|3-9-5|3-9-6|3-9-7|3-9-8|3-9-9|4-0-0|4-0-1|4-0-2|4-0-3|4-0-4|4-0-5|4-0-6|4-0-7|4-0-8|4-0-9|4-1-0|4-1-1|4-1-2|4-1-3|4-1-4|4-1-5|4-1-6|4-1-7|4-1-8|4-1-9|4-2-0|4-2-1|4-2-2|4-2-3|4-2-4|4-2-5|4-2-6|4-2-7|4-2-8|4-2-9|4-3-0|4-3-1|4-3-2|4-3-3|4-3-4|4-3-5|4-3-6|4-3-7|4-3-8|4-3-9|4-4-0|4-4-1|4-4-2|4-4-3|4-4-4|4-4-5|4-4-6|4-4-7|4-4-8|4-4-9|4-5-0|4-5-1|4-5-2|4-5-3|4-5-4|4-5-5|4-5-6|4-5-7|4-5-8|4-5-9|4-6-0|4-6-1|4-6-2|4-6-3|4-6-4|4-6-5|4-6-6|4-6-7|4-6-8|4-6-9|4-7-0|4-7-1|4-7-2|4-7-3|4-7-4|4-7-5|4-7-6|4-7-7|4-7-8|4-7-9|4-8-0|4-8-1|4-8-2|4-8-3|4-8-4|4-8-5|4-8-6|4-8-7|4-8-8|4-8-9|4-9-0|4-9-1|4-9-2|4-9-3|4-9-4|4-9-5|4-9-6|4-9-7|4-9-8|4-9-9|5-0-0|5-0-1|5-0-2|5-0-3|5-0-4|5-0-5|5-0-6|5-0-7|5-0-8|5-0-9|5-1-0|5-1-1|5-1-2|5-1-3|5-1-4|5-1-5|5-1-6|5-1-7|5-1-8|5-1-9|5-2-0|5-2-1|5-2-2|5-2-3|5-2-4|5-2-5|5-2-6|5-2-7|5-2-8|5-2-9|5-3-0|5-3-1|5-3-2|5-3-3|5-3-4|5-3-5|5-3-6|5-3-7|5-3-8|5-3-9|5-4-0|5-4-1|5-4-2|5-4-3|5-4-4|5-4-5|5-4-6|5-4-7|5-4-8|5-4-9|5-5-0|5-5-1|5-5-2|5-5-3|5-5-4|5-5-5|5-5-6|5-5-7|5-5-8|5-5-9|5-6-0|5-6-1|5-6-2|5-6-3|5-6-4|5-6-5|5-6-6|5-6-7|5-6-8|5-6-9|5-7-0|5-7-1|5-7-2|5-7-3|5-7-4|5-7-5|5-7-6|5-7-7|5-7-8|5-7-9|5-8-0|5-8-1|5-8-2|5-8-3|5-8-4|5-8-5|5-8-6|5-8-7|5-8-8|5-8-9|5-9-0|5-9-1|5-9-2|5-9-3|5-9-4|5-9-5|5-9-6|5-9-7|5-9-8|5-9-9|6-0-0|6-0-1|6-0-2|6-0-3|6-0-4|6-0-5|6-0-6|6-0-7|6-0-8|6-0-9|6-1-0|6-1-1|6-1-2|6-1-3|6-1-4|6-1-5|6-1-6|6-1-7|6-1-8|6-1-9|6-2-0|6-2-1|6-2-2|6-2-3|6-2-4|6-2-5|6-2-6|6-2-7|6-2-8|6-2-9|6-3-0|6-3-1|6-3-2|6-3-3|6-3-4|6-3-5|6-3-6|6-3-7|6-3-8|6-3-9|6-4-0|6-4-1|6-4-2|6-4-3|6-4-4|6-4-5|6-4-6|6-4-7|6-4-8|6-4-9|6-5-0|6-5-1|6-5-2|6-5-3|6-5-4|6-5-5|6-5-6|6-5-7|6-5-8|6-5-9|6-6-0|6-6-1|6-6-2|6-6-3|6-6-4|6-6-5|6-6-6|6-6-7|6-6-8|6-6-9|6-7-0|6-7-1|6-7-2|6-7-3|6-7-4|6-7-5|6-7-6|6-7-7|6-7-8|6-7-9|6-8-0|6-8-1|6-8-2|6-8-3|6-8-4|6-8-5|6-8-6|6-8-7|6-8-8|6-8-9|6-9-0|6-9-1|6-9-2|6-9-3|6-9-4|6-9-5|6-9-6|6-9-7|6-9-8|6-9-9|7-0-0|7-0-1|7-0-2|7-0-3|7-0-4|7-0-5|7-0-6|7-0-7|7-0-8|7-0-9|7-1-0|7-1-1|7-1-2|7-1-3|7-1-4|7-1-5|7-1-6|7-1-7|7-1-8|7-1-9|7-2-0|7-2-1|7-2-2|7-2-3|7-2-4|7-2-5|7-2-6|7-2-7|7-2-8|7-2-9|7-3-0|7-3-1|7-3-2|7-3-3|7-3-4|7-3-5|7-3-6|7-3-7|7-3-8|7-3-9|7-4-0|7-4-1|7-4-2|7-4-3|7-4-4|7-4-5|7-4-6|7-4-7|7-4-8|7-4-9|7-5-0|7-5-1|7-5-2|7-5-3|7-5-4|7-5-5|7-5-6|7-5-7|7-5-8|7-5-9|7-6-0|7-6-1|7-6-2|7-6-3|7-6-4|7-6-5|7-6-6|7-6-7|7-6-8|7-6-9|7-7-0|7-7-1|7-7-2|7-7-3|7-7-4|7-7-5|7-7-6|7-7-7|7-7-8|7-7-9|7-8-0|7-8-1|7-8-2|7-8-3|7-8-4|7-8-5|7-8-6|7-8-7|7-8-8|7-8-9|7-9-0|7-9-1|7-9-2|7-9-3|7-9-4|7-9-5|7-9-6|7-9-7|7-9-8|7-9-9|8-0-0|8-0-1|8-0-2|8-0-3|8-0-4|8-0-5|8-0-6|8-0-7|8-0-8|8-0-9|8-1-0|8-1-1|8-1-2|8-1-3|8-1-4|8-1-5|8-1-6|8-1-7|8-1-8|8-1-9|8-2-0|8-2-1|8-2-2|8-2-3|8-2-4|8-2-5|8-2-6|8-2-7|8-2-8|8-2-9|8-3-0|8-3-1|8-3-2|8-3-3|8-3-4|8-3-5|8-3-6|8-3-7|8-3-8|8-3-9|8-4-0|8-4-1|8-4-2|8-4-3|8-4-4|8-4-5|8-4-6|8-4-7|8-4-8|8-4-9|8-5-0|8-5-1|8-5-2|8-5-3|8-5-4|8-5-5|8-5-6|8-5-7|8-5-8|8-5-9|8-6-0|8-6-1|8-6-2|8-6-3|8-6-4|8-6-5|8-6-6|8-6-7|8-6-8|8-6-9|8-7-0|8-7-1|8-7-2|8-7-3|8-7-4|8-7-5|8-7-6|8-7-7|8-7-8|8-7-9|8-8-0|8-8-1|8-8-2|8-8-3|8-8-4|8-8-5|8-8-6|8-8-7|8-8-8|8-8-9|8-9-0|8-9-1|8-9-2|8-9-3|8-9-4|8-9-5|8-9-6|8-9-7|8-9-8|8-9-9|9-0-0|9-0-1|9-0-2|9-0-3|9-0-4|9-0-5|9-0-6|9-0-7|9-0-8|9-0-9|9-1-0|9-1-1|9-1-2|9-1-3|9-1-4|9-1-5|9-1-6|9-1-7|9-1-8|9-1-9|9-2-0|9-2-1|9-2-2|9-2-3|9-2-4|9-2-5|9-2-6|9-2-7|9-2-8|9-2-9|9-3-0|9-3-1|9-3-2|9-3-3|9-3-4|9-3-5|9-3-6|9-3-7|9-3-8|9-3-9|9-4-0|9-4-1|9-4-2|9-4-3|9-4-4|9-4-5|9-4-6|9-4-7|9-4-8|9-4-9|9-5-0|9-5-1|9-5-2|9-5-3|9-5-4|9-5-5|9-5-6|9-5-7|9-5-8|9-5-9|9-6-0|9-6-1|9-6-2|9-6-3|9-6-4|9-6-5|9-6-6|9-6-7|9-6-8|9-6-9|9-7-0|9-7-1|9-7-2|9-7-3|9-7-4|9-7-5|9-7-6|9-7-7|9-7-8|9-7-9|9-8-0|9-8-1|9-8-2|9-8-3|9-8-4|9-8-5|9-8-6|9-8-7|9-8-8|9-8-9|9-9-0|9-9-1|9-9-2|9-9-3|9-9-4|9-9-5|9-9-6|9-9-7|9-9-8|9-9-9"`;
diff --git a/tests/index.test-types.ts b/src/__tests__/index.test-types.ts
similarity index 94%
rename from tests/index.test-types.ts
rename to src/__tests__/index.test-types.ts
index f23c57c..2290b48 100644
--- a/tests/index.test-types.ts
+++ b/src/__tests__/index.test-types.ts
@@ -4,7 +4,8 @@
* the file needs to compile without error.
*/
import { expectType, expectNever, TypeEqual } from 'ts-expect';
-import Soit, { Infer } from '../src/index';
+import Soit, { Infer } from '../index';
+import { _soitTemplate } from '../template';
/**
* Should prevent unknown literals array
@@ -26,7 +27,7 @@ import Soit, { Infer } from '../src/index';
/**
* Should prevent unknown object inference
*/
-{
+/*{
const randomFunction = () => {};
const randomArray = [''];
const randomObject = {};
@@ -43,7 +44,7 @@ import Soit, { Infer } from '../src/index';
// @ts-expect-error
type Fail4 = Infer;
-}
+}*/
/**
* Should guard and infer any given string literal
@@ -231,7 +232,7 @@ import Soit, { Infer } from '../src/index';
}
/**
- * Sould be iterable
+ * Should be iterable
*/
{
const isSet1 = Soit(['one', 'two', 'three']);
@@ -241,6 +242,18 @@ import Soit, { Infer } from '../src/index';
expectType>(true);
}
+/**
+ * Template should be iterable
+ */
+{
+ const isSet1 = Soit(['one', 'two', 'three']);
+ const isTemplate = Soit.Template(isSet1, '-', isSet1);
+ type Template = Infer;
+
+ const templatePossibleValues = Array.from(isTemplate);
+ expectType>(true);
+}
+
/**
* Should expose map and forEach
*/
diff --git a/tests/index.test.ts b/src/__tests__/index.test.ts
similarity index 84%
rename from tests/index.test.ts
rename to src/__tests__/index.test.ts
index a984d5e..b8b94ef 100644
--- a/tests/index.test.ts
+++ b/src/__tests__/index.test.ts
@@ -1,4 +1,4 @@
-import Soit from '../src/index';
+import Soit from '../index';
describe('Soit', () => {
it('should guard for any given string literal', () => {
@@ -93,4 +93,19 @@ describe('Soit', () => {
expect(isDifferenceSet('four')).toBe(false);
expect(Array.from(isDifferenceSet)).toEqual(['one', 'two']);
});
+ it('should be able to create a template', () => {
+ const isSet = Soit(['one', 'two', 'three']);
+ const isTemplate = Soit.Template('$', isSet);
+ expect(Array.from(isTemplate)).toEqual(['$one', '$two', '$three']);
+ expect(isTemplate('$one')).toBe(true);
+ expect(isTemplate('$two')).toBe(true);
+ expect(isTemplate('$three')).toBe(true);
+ expect(isTemplate('$four')).toBe(false);
+ expect(isTemplate('one')).toBe(false);
+ expect(isTemplate.capture('$one')).toEqual(['one']);
+ expect(isTemplate.capture('$two')).toEqual(['two']);
+ expect(isTemplate.capture('$three')).toEqual(['three']);
+ expect(isTemplate.capture('$four')).toBe(null);
+ expect(isTemplate.capture('one')).toBe(null);
+ });
});
diff --git a/src/__tests__/template.test.ts b/src/__tests__/template.test.ts
new file mode 100644
index 0000000..030c16a
--- /dev/null
+++ b/src/__tests__/template.test.ts
@@ -0,0 +1,104 @@
+import { _soitCore } from '../core';
+import { _soitTemplate } from '../template';
+import Soit from '../index';
+
+describe('SoitTemplate', () => {
+ it('should test a simple template', () => {
+ const mapMock = jest.fn();
+ const forEachMock = jest.fn();
+ const template = _soitTemplate(
+ _soitCore(['a', 'b']),
+ '-',
+ _soitCore(['c', 'd'])
+ );
+ expect(template('a-c')).toBe(true);
+ expect(template('a-d')).toBe(true);
+ expect(template('b-c')).toBe(true);
+ expect(template('b-d')).toBe(true);
+ expect(template('a')).toBe(false);
+ expect(template('c')).toBe(false);
+ expect(template('a-d-e')).toBe(false);
+ expect(Array.from(template)).toEqual(['a-c', 'a-d', 'b-c', 'b-d']);
+ expect(template.capture('a-d')).toEqual(['a', 'd']);
+ expect(template.capture('ad')).toEqual(null);
+ expect(template.capture('1-2')).toEqual(null);
+
+ template.map(mapMock);
+ expect(mapMock).toHaveBeenCalledTimes(4);
+ expect(mapMock).toHaveBeenNthCalledWith(1, 'a-c', 0, [
+ 'a-c',
+ 'a-d',
+ 'b-c',
+ 'b-d',
+ ]);
+ expect(mapMock).toHaveBeenNthCalledWith(2, 'a-d', 1, [
+ 'a-c',
+ 'a-d',
+ 'b-c',
+ 'b-d',
+ ]);
+ expect(mapMock).toHaveBeenNthCalledWith(3, 'b-c', 2, [
+ 'a-c',
+ 'a-d',
+ 'b-c',
+ 'b-d',
+ ]);
+ expect(mapMock).toHaveBeenNthCalledWith(4, 'b-d', 3, [
+ 'a-c',
+ 'a-d',
+ 'b-c',
+ 'b-d',
+ ]);
+
+ template.forEach(forEachMock);
+ expect(forEachMock).toHaveBeenCalledTimes(4);
+ expect(forEachMock).toHaveBeenNthCalledWith(1, 'a-c', 0, [
+ 'a-c',
+ 'a-d',
+ 'b-c',
+ 'b-d',
+ ]);
+ expect(forEachMock).toHaveBeenNthCalledWith(2, 'a-d', 1, [
+ 'a-c',
+ 'a-d',
+ 'b-c',
+ 'b-d',
+ ]);
+ expect(forEachMock).toHaveBeenNthCalledWith(3, 'b-c', 2, [
+ 'a-c',
+ 'a-d',
+ 'b-c',
+ 'b-d',
+ ]);
+ expect(forEachMock).toHaveBeenNthCalledWith(4, 'b-d', 3, [
+ 'a-c',
+ 'a-d',
+ 'b-c',
+ 'b-d',
+ ]);
+ });
+ it('should test a complex template', () => {
+ const mapMock = jest.fn();
+ const forEachMock = jest.fn();
+ const digit = Soit([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ const template = Soit.Template(digit, '-', digit, '-', digit);
+ expect(template('1-2-3')).toBe(true);
+ const result = template.capture('1-2-3');
+ if (result) {
+ const [a, b, c] = result;
+ expect(a).toBe('1');
+ expect(b).toBe('2');
+ expect(c).toBe('3');
+ }
+ expect(template.capture('1-2-3')).toEqual(['1', '2', '3']);
+ expect(Array.from(template).join('|')).toMatchSnapshot();
+ expect(template.capture('a-b-c')).toBe(null);
+ expect(template.capture('123')).toBe(null);
+
+ template.map(mapMock);
+ expect(mapMock).toHaveBeenCalledTimes(1000);
+
+ template.forEach(forEachMock);
+ expect(forEachMock).toHaveBeenCalledTimes(1000);
+ });
+});
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000..bf6131f
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1 @@
+export const SOIT_SYMBOL = Symbol();
diff --git a/src/core.ts b/src/core.ts
new file mode 100644
index 0000000..a35def6
--- /dev/null
+++ b/src/core.ts
@@ -0,0 +1,35 @@
+import { ArrayUtils, Literal, SetUtils, Soit } from './types/core.types';
+import { SOIT_SYMBOL } from './constants';
+
+export function _soitCore(values: readonly V[]): Soit {
+ const _set = new Set(values);
+
+ const _values = Array.from(_set);
+
+ function _guard(testedValue: Literal): testedValue is V {
+ return values.some(o => o === testedValue);
+ }
+
+ const _arrayUtils: ArrayUtils = {
+ forEach: (...args) => _values.forEach(...args),
+ map: (...args) => _values.map(...args),
+ };
+
+ const _setUtils: SetUtils = {
+ subset: subsetValues => _soitCore(subsetValues),
+ extend: additionalValues => _soitCore([..._values, ...additionalValues]),
+ // we cannot rely on filter and includes typings
+ difference: differenceValues =>
+ _soitCore(
+ _values.filter(value => !differenceValues.includes(value as any)) as any
+ ),
+ };
+
+ const _iterable: Iterable = {
+ [Symbol.iterator]: () => _set[Symbol.iterator](),
+ };
+
+ return Object.assign(_guard, _iterable, _setUtils, _arrayUtils, {
+ [SOIT_SYMBOL]: true,
+ });
+}
diff --git a/src/index.ts b/src/index.ts
index 5da9e5f..df16803 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,110 +1,45 @@
-type Literal = string | number | boolean;
-
-/* This type ensures that the input type is not a set of unknown values. */
-type SubLiteral = string extends T
- ? never
- : number extends T
- ? never
- : boolean extends T
- ? never
- : Literal;
-
-type Guard = {
- /**
- * Uses the `Soit` instance as a type guard:
- * ```ts
- * const is123 = Soit([1, 2, 3]);
- * if(is123(value)) { ... }
- * ```
- *
- * @param {Literal} value
- * @returns {boolean} true or false
- */
- (testedValue: Literal): testedValue is V;
-};
-
-type ArrayUtils = Pick, 'forEach' | 'map'>;
-
-type SetUtils = {
- /**
- * Creates a subset of the current set.
- *
- * @param values (array of Literals)
- * @returns `Soit` instance
- */
- subset: (subsetValues: readonly S[]) => Soit;
- /**
- * Creates a extended set from the current set.
- *
- * @param values (array of Literals)
- * @returns `Soit` instance
- */
- extend: >(
- additionalValues: readonly A[]
- ) => Soit;
- /**
- * Creates a new set from the current set by excluding values.
- *
- * @param values (array of Literals)
- * @returns `Soit` instance
- */
- difference: >(
- differenceValues: readonly D[]
- ) => Soit>;
-};
-
-/* This type aims to display an alias when manipulating a Soit instance. */
-type Soit = Guard &
- Iterable &
- ArrayUtils &
- SetUtils;
-
-function _soit(values: readonly V[]) {
- const _set = new Set(values);
-
- const _values = Array.from(_set);
-
- function _guard(testedValue: Literal): testedValue is V {
- return values.some(o => o === testedValue);
- }
-
- const _arrayUtils: ArrayUtils = {
- forEach: (...args) => _values.forEach(...args),
- map: (...args) => _values.map(...args),
- };
-
- const _setUtils: SetUtils = {
- subset: subsetValues => _soit(subsetValues),
- extend: additionalValues => _soit([..._values, ...additionalValues]),
- // we cannot rely on filter and includes typings
- difference: differenceValues =>
- _soit(
- _values.filter(value => !differenceValues.includes(value as any)) as any
- ),
- };
-
- const _iterable: Iterable = {
- [Symbol.iterator]: () => _set[Symbol.iterator](),
- };
-
- return Object.assign(_guard, _iterable, _setUtils, _arrayUtils);
-}
+import { Literal, Primitive, Soit } from './types/core.types';
+import {
+ PrimitiveTemplate,
+ SoitTemplate,
+ TemplateValues,
+} from './types/template.types';
+import { _soitCore } from './core';
+import { _soitTemplate } from './template';
/**
- * Creates a new `Soit` instance with the given set of values.
+ * Creates a `Soit` instance with the given set of values.
*
* ```ts
* const is123 = Soit([1, 2, 3]);
- * if(is123(value)) { ... }
* ```
*
* @param values array of Literals
* @returns `Soit` instance
*/
-function Soit>(values: readonly V[]): Soit {
- return _soit(values);
+function Soit>(values: readonly V[]): Soit {
+ return _soitCore(values);
}
-export type Infer = S extends Soit ? V : never;
+/**
+ * Creates a `SoitTemplate` instance with the given template.
+ *
+ * ```ts
+ * const isBorderWidth = Soit.Template(is123, 'px');
+ * ```
+ *
+ * The `isBorderWidth` instance will only accept the following values: `1px`, `2px`, `3px`.
+ *
+ * @returns `SoitTemplate` instance
+ */
+function Template>(
+ ...templateValues: T
+): SoitTemplate {
+ return _soitTemplate(...templateValues);
+}
+
+Object.assign(Soit, { Template });
+
+export default Soit as typeof Soit & { Template: typeof Template };
-export default Soit;
+export type { Infer } from './types/index.types';
diff --git a/src/template.ts b/src/template.ts
new file mode 100644
index 0000000..8f063fc
--- /dev/null
+++ b/src/template.ts
@@ -0,0 +1,55 @@
+import { ArrayUtils } from './types/core.types';
+import {
+ SoitTemplate,
+ TemplateUtils,
+ TemplateValues,
+ ValuesFromTemplate,
+} from './types/template.types';
+import { generateValuesFromTemplate } from './utils/generateValuesFromTemplate';
+import { getRegExpFromValues } from './utils/getRegExpFromValues';
+import { isSoit } from './utils/isSoit';
+
+export function _soitTemplate(
+ ...template: T
+): SoitTemplate {
+ const _set = new Set>(
+ generateValuesFromTemplate(template)
+ );
+
+ const _values = Array.from(_set);
+
+ function _guard(testedValue: string): testedValue is ValuesFromTemplate {
+ return getRegExpFromValues(template).test(testedValue);
+ }
+
+ const _arrayUtils: ArrayUtils> = {
+ forEach: (...args) => _values.forEach(...args),
+ map: (...args) => _values.map(...args),
+ };
+
+ const _templateUtils: TemplateUtils = {
+ capture: (testedValue: string) => {
+ const result = getRegExpFromValues(template).exec(testedValue);
+
+ if (result === null) {
+ return null;
+ }
+
+ const captureGroupCount = template.filter(isSoit).length;
+
+ let captures: any = [];
+
+ for (let i = 1; i <= captureGroupCount; i++) {
+ captures.push(result[i]);
+ }
+
+ return captures;
+ },
+ };
+
+ const _iterable: Iterable> = {
+ [Symbol.iterator]: () => _set[Symbol.iterator](),
+ };
+
+ return Object.assign(_guard, _iterable, _arrayUtils, _templateUtils);
+}
diff --git a/src/types/__tests__/core.test-types.ts b/src/types/__tests__/core.test-types.ts
new file mode 100644
index 0000000..619eb66
--- /dev/null
+++ b/src/types/__tests__/core.test-types.ts
@@ -0,0 +1,36 @@
+import { TypeEqual, expectType } from 'ts-expect';
+import { Primitive } from '../core.types';
+
+/**
+ * Primitive
+ */
+/* should return never if unknown string */
+{
+ type Result = Primitive;
+ expectType>(true);
+}
+/* should return never if unknown number */
+{
+ type Result = Primitive;
+ expectType>(true);
+}
+/* should return never if unknown boolean */
+{
+ type Result = Primitive;
+ expectType>(true);
+}
+/* should return unknown if defined string */
+{
+ type Result = Primitive<'abc'>;
+ expectType>(true);
+}
+/* should return unknown if defined number */
+{
+ type Result = Primitive<123>;
+ expectType>(true);
+}
+/* should return unknown if defined boolean */
+{
+ type Result = Primitive;
+ expectType>(true);
+}
diff --git a/src/types/__tests__/index.test-types.ts b/src/types/__tests__/index.test-types.ts
new file mode 100644
index 0000000..352f360
--- /dev/null
+++ b/src/types/__tests__/index.test-types.ts
@@ -0,0 +1,27 @@
+import { TypeEqual, expectType } from 'ts-expect';
+import Soit, { Infer } from '../..';
+
+/* Infer generic should work for both Soit and SoitTemplate */
+{
+ // GIVEN
+ const isDigit = Soit([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
+ const template = Soit.Template(isDigit, '-', isDigit, '-', isDigit);
+
+ // WHEN
+ type Digit = Infer;
+ type Template = Infer;
+
+ //THEN
+ expectType>(true);
+}
+/* Infer generic shoud return never if not Soit or SoitTemplate */
+{
+ // GIVEN
+ const object = {};
+
+ // WHEN
+ type Object = Infer;
+
+ // THEN
+ expectType>(true);
+}
diff --git a/src/types/__tests__/template.test-types.ts b/src/types/__tests__/template.test-types.ts
new file mode 100644
index 0000000..2b6b8d9
--- /dev/null
+++ b/src/types/__tests__/template.test-types.ts
@@ -0,0 +1,263 @@
+import { TypeEqual, TypeOf, expectType } from 'ts-expect';
+import { Soit } from '../core.types';
+import {
+ ValuesFromTemplate,
+ CapturedGroupsFromTemplate,
+ PrimitiveTemplate,
+ CaptureGroupsFromTemplate,
+ NextGroups,
+} from '../template.types';
+import { _soitTemplate } from '../../template';
+
+/**
+ * PrimitiveTemplate
+ */
+/* should return never if contains unknown string */
+{
+ expectType, never>>(true);
+ expectType, never>>(true);
+ expectType, never>>(true);
+ expectType, never>>(true);
+ expectType<
+ TypeEqual<
+ PrimitiveTemplate<[Soit<'a' | 'b' | 'c'>, string, Soit<1 | 2 | 3>]>,
+ never
+ >
+ >(true);
+}
+/* should return never if contains unknown number */
+{
+ expectType, never>>(true);
+ expectType, never>>(true);
+ expectType, never>>(true);
+ expectType, never>>(true);
+ expectType<
+ TypeEqual<
+ PrimitiveTemplate<[Soit<'a' | 'b' | 'c'>, number, Soit<1 | 2 | 3>]>,
+ never
+ >
+ >(true);
+}
+/* should return never if contains unknown boolean */
+{
+ expectType, never>>(true);
+ expectType, never>>(true);
+ expectType, never>>(true);
+ expectType, never>>(true);
+ expectType<
+ TypeEqual<
+ PrimitiveTemplate<[Soit<'a' | 'b' | 'c'>, boolean, Soit<1 | 2 | 3>]>,
+ never
+ >
+ >(true);
+}
+/* should return unknown if primitive */
+{
+ expectType, unknown>>(true);
+ expectType, unknown>>(true);
+ expectType, unknown>>(true);
+ expectType, unknown>>(true);
+}
+
+/**
+ * ValuesFromTemplate
+ */
+/* should return a union with all possible values from template (no Soit) */
+{
+ type Result = ValuesFromTemplate<['simple', 'string', 'template']>;
+ expectType>(true);
+}
+/* should return a union with all possible values from template (one Soit) */
+{
+ type Result = ValuesFromTemplate<
+ [Soit<'uncertain' | 'variable'>, 'string', 'template']
+ >;
+ expectType<
+ TypeEqual
+ >(true);
+}
+/* should return a union with all possible values from template (many Soit) */
+{
+ type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
+ type Result = ValuesFromTemplate<
+ [Soit, '-', Soit, '-', Soit]
+ >;
+ expectType>(true);
+}
+
+/**
+ * NextGroups
+ */
+/* should capture the first value */
+{
+ type Result = NextGroups<
+ Soit<'uncertain' | 'variable'>,
+ 'uncertainstringtemplate',
+ '',
+ [],
+ true
+ >;
+ expectType>(true);
+}
+/* should capture the first value (number) */
+{
+ type Result = NextGroups, '100px', '', [], true>;
+ expectType>(true);
+}
+/* should capture the first value (boolean) */
+{
+ type Result = NextGroups, 'true!', '', [], true>;
+ expectType>(true);
+}
+/* should not capture partial value if no rest expected */
+{
+ type Result = NextGroups<
+ Soit<'uncertain' | 'variable'>,
+ 'uncertainstringtemplate',
+ '',
+ []
+ >;
+ expectType>(true);
+}
+/* should capture whole value if no rest expected */
+{
+ type Result = NextGroups, 'uncertain', '', []>;
+ expectType>(true);
+}
+/* should capture last part if no rest expected */
+{
+ type Result = NextGroups, '100em', '100', []>;
+ expectType>(true);
+}
+/* should capture return null if capture group dont match */
+{
+ type Result = NextGroups<
+ Soit<'uncertain' | 'variable'>,
+ 'unknownstringtemplate',
+ '',
+ [],
+ true
+ >;
+ expectType>(true);
+}
+/* should accumulate captured values */
+{
+ type Result = NextGroups, '100px', '100', ['100'], true>;
+ expectType>(true);
+}
+/* should keep previous captured values */
+{
+ type Result = NextGroups<
+ 'string',
+ 'uncertainstringtemplate',
+ 'uncertain',
+ ['uncertain'],
+ true
+ >;
+ expectType>(true);
+}
+/* should keep previous captured values (with no rest) */
+{
+ type Result = NextGroups<
+ 'template',
+ 'uncertainstringtemplate',
+ 'uncertainstring',
+ ['uncertain']
+ >;
+ expectType>(true);
+}
+/* should loose previous captured values if no match */
+{
+ type Result = NextGroups<
+ 'string',
+ 'uncertainstringtemplate',
+ 'uncertain',
+ ['uncertain']
+ >;
+ expectType>(true);
+}
+
+/**
+ * CaptureGroupsFromTemplate
+ */
+/* should return empty capture tuple if no Soit in template */
+{
+ // GIVEN
+ type TemplateValues = ['simple', 'string', 'template'];
+
+ // WHEN
+ type Result = CaptureGroupsFromTemplate;
+
+ // THEN
+ expectType>(true);
+}
+/* should return capture tuple with one value if one Soit in template */
+{
+ // GIVEN
+ type TemplateValues = [Soit<'uncertain' | 'variable'>, 'string', 'template'];
+
+ // WHEN
+ type Result = CaptureGroupsFromTemplate;
+
+ // THEN
+ expectType>(true);
+}
+/* should return capture tuple with two values if two Soit in template */
+{
+ // GIVEN
+ type TemplateValues = [Soit, '-', Soit<1 | 2 | 3>];
+
+ // WHEN
+ type Result = CaptureGroupsFromTemplate;
+
+ // THEN
+ expectType>(true);
+ expectType>(true);
+}
+
+/**
+ * CapturedGroupsFromTemplate
+ */
+/* should return empty capture tuple if no Soit in template */
+{
+ // GIVEN
+ type TemplateValues = ['simple', 'string', 'template'];
+
+ // WHEN
+ type Result = CapturedGroupsFromTemplate<
+ TemplateValues,
+ ValuesFromTemplate
+ >;
+
+ // THEN
+ expectType>(true);
+}
+/* should return capture tuple with one value if one Soit in template */
+{
+ // GIVEN
+ type TemplateValues = [Soit<'uncertain' | 'variable'>, 'string', 'template'];
+
+ // WHEN
+ type Result = CapturedGroupsFromTemplate<
+ TemplateValues,
+ ValuesFromTemplate
+ >;
+
+ // THEN
+ expectType>(true);
+}
+/* should return capture tuple with two values if two Soit in template */
+{
+ // GIVEN
+ type TemplateValues = [Soit, '-', Soit<1 | 2 | 3>];
+
+ // WHEN
+ type Result = CapturedGroupsFromTemplate<
+ TemplateValues,
+ ValuesFromTemplate
+ >;
+
+ // THEN
+ expectType>(true);
+ expectType>(true);
+}
diff --git a/src/types/core.types.ts b/src/types/core.types.ts
new file mode 100644
index 0000000..db3caf9
--- /dev/null
+++ b/src/types/core.types.ts
@@ -0,0 +1,60 @@
+export type Literal = string | number | boolean;
+
+/* Ensures that the input type is a finite primitive. */
+export type Primitive = string extends T
+ ? never
+ : number extends T
+ ? never
+ : boolean extends T
+ ? never
+ : unknown;
+
+export type Guard = {
+ /**
+ * Uses the `Soit` instance as a type guard:
+ * ```ts
+ * const is123 = Soit([1, 2, 3]);
+ * if(is123(value)) { ... }
+ * ```
+ *
+ * @param {Literal} value
+ * @returns {boolean} true or false
+ */
+ (testedValue: Literal): testedValue is V;
+};
+
+export type ArrayUtils = Pick, 'forEach' | 'map'>;
+
+export type SetUtils = {
+ /**
+ * Creates a subset of the current set.
+ *
+ * @param values (array of Literals)
+ * @returns `Soit` instance
+ */
+ subset: >(subsetValues: readonly S[]) => Soit;
+ /**
+ * Creates a extended set from the current set.
+ *
+ * @param values (array of Literals)
+ * @returns `Soit` instance
+ */
+ extend: >(
+ additionalValues: readonly A[]
+ ) => Soit;
+ /**
+ * Creates a new set from the current set by excluding values.
+ *
+ * @param values (array of Literals)
+ * @returns `Soit` instance
+ */
+ difference: >(
+ differenceValues: readonly D[]
+ ) => Soit>;
+};
+
+/* This type aims to display an alias when manipulating a Soit instance. */
+export type Soit = Guard &
+ Iterable &
+ ArrayUtils &
+ SetUtils;
diff --git a/src/types/index.types.ts b/src/types/index.types.ts
new file mode 100644
index 0000000..7795814
--- /dev/null
+++ b/src/types/index.types.ts
@@ -0,0 +1,9 @@
+import { Soit } from './core.types';
+import { SoitTemplate, ValuesFromTemplate } from './template.types';
+
+export type Infer = // TODO: constraint S to Soit or SoitTemplate
+ S extends Soit
+ ? V
+ : S extends SoitTemplate
+ ? ValuesFromTemplate
+ : never;
diff --git a/src/types/template.types.ts b/src/types/template.types.ts
new file mode 100644
index 0000000..065fdbb
--- /dev/null
+++ b/src/types/template.types.ts
@@ -0,0 +1,127 @@
+import { ArrayUtils, Literal, Primitive, Soit } from './core.types';
+
+/* Represents a part/chunk of a template. */
+export type TemplateValue = Literal | Soit;
+
+/* Represents the template. */
+export type TemplateValues = readonly [TemplateValue, ...TemplateValue[]];
+
+/* Ensures that all parts/chunks of a template is a finite primitive. */
+export type PrimitiveTemplate =
+ T[number] extends Soit | Primitive ? unknown : never;
+
+/* Converts a template value to be used in a template literal. */
+type TemplateValueToString = V extends Soit
+ ? LiteralToString
+ : LiteralToString;
+
+/* Converts a literal into a string. */
+type LiteralToString = L extends Literal
+ ? `${L}`
+ : never;
+
+/* Get the union of all possible values from a template. */
+export type ValuesFromTemplate<
+ T extends TemplateValues,
+ Acc extends string = '',
+> = T extends [
+ infer T0 extends Literal | Soit,
+ ...infer TRest extends TemplateValues,
+]
+ ? ValuesFromTemplate}`>
+ : `${Acc}${TemplateValueToString}`;
+
+/* Get all the possible capture groups from a template. */
+export type CaptureGroupsFromTemplate<
+ T extends TemplateValues,
+ Acc extends string[] = [],
+> = T extends [
+ infer T0 extends Literal | Soit,
+ ...infer TRest extends TemplateValues,
+]
+ ? CaptureGroupsFromTemplate<
+ TRest,
+ T0 extends Soit ? [...Acc, LiteralToString] : Acc
+ >
+ : T[0] extends Soit
+ ? [...Acc, LiteralToString] | null
+ : Acc | null;
+
+type RestToken = WithRest extends true ? string : '';
+
+/* Get the accumulated capture groups for the current recursive loop. */
+export type NextGroups<
+ TChunk extends TemplateValue,
+ TestedValues extends string,
+ AccValues extends string,
+ AccGroups extends string[],
+ WithRest extends boolean = false,
+> = TChunk extends Soit
+ ? TestedValues extends `${AccValues}${TargetValues}${infer Rest extends
+ RestToken}`
+ ? TestedValues extends `${AccValues}${infer Group}${Rest}`
+ ? [...AccGroups, Group]
+ : null
+ : null
+ : TestedValues extends `${AccValues}${LiteralToString}${RestToken}`
+ ? AccGroups
+ : null;
+
+/* Get the expected capture groups for the tested value. */
+export type CapturedGroupsFromTemplate<
+ T extends TemplateValues,
+ TestedValues extends string,
+ AccValues extends string = '',
+ AccGroups extends string[] | null = [],
+> = string extends TestedValues
+ ? CaptureGroupsFromTemplate // if the tested value is a string, return all possible capture groups
+ : AccGroups extends unknown[]
+ ? T extends [
+ infer T0 extends Literal | Soit,
+ ...infer TRest extends TemplateValues,
+ ]
+ ? CapturedGroupsFromTemplate<
+ TRest,
+ TestedValues,
+ `${AccValues}${TemplateValueToString}`,
+ NextGroups
+ >
+ : NextGroups
+ : null;
+
+export type TemplateUtils = {
+ /**
+ * Uses the `SoitTemplate` instance to capture the values from a string:
+ * ```ts
+ * const isGetter = Soit.Template('get', Soit(['Seconds', 'Minutes', 'Hours']));
+ * const [unit] = isGetter.capture('getSeconds');
+ * ```
+ *
+ * @param {string} value
+ * @returns {array | null} array of captured values or null
+ */
+ capture: (
+ testedValue: V
+ ) => CapturedGroupsFromTemplate;
+};
+
+export type TemplateGuard = {
+ /**
+ * Uses the `SoitTemplate` instance as a type guard:
+ * ```ts
+ * const isGetter = Soit.Template('get', Soit(['Seconds', 'Minutes', 'Hours']));
+ * if(isGetter(method)) { ... }
+ * ```
+ *
+ * @param {string} value
+ * @returns {boolean} true or false
+ */
+ (testedValue: string): testedValue is V;
+};
+
+export type SoitTemplate = TemplateGuard<
+ ValuesFromTemplate
+> &
+ Iterable> &
+ ArrayUtils> &
+ TemplateUtils;
diff --git a/src/utils/__tests__/escapeRegExp.test.ts b/src/utils/__tests__/escapeRegExp.test.ts
new file mode 100644
index 0000000..d0309c0
--- /dev/null
+++ b/src/utils/__tests__/escapeRegExp.test.ts
@@ -0,0 +1,24 @@
+import { escapeRegExp } from '../escapeRegExp';
+
+describe('escapeRegExp', () => {
+ it('should escape special characters to be used in a regular expression', () => {
+ expect(escapeRegExp('a')).toBe('a');
+ expect(escapeRegExp('a.b')).toBe('a\\.b');
+ expect(escapeRegExp('a-b')).toBe('a\\-b');
+ expect(escapeRegExp('a^b')).toBe('a\\^b');
+ expect(escapeRegExp('a$b')).toBe('a\\$b');
+ expect(escapeRegExp('a*b')).toBe('a\\*b');
+ expect(escapeRegExp('a+b')).toBe('a\\+b');
+ expect(escapeRegExp('a?b')).toBe('a\\?b');
+ expect(escapeRegExp('a.b')).toBe('a\\.b');
+ expect(escapeRegExp('a(b')).toBe('a\\(b');
+ expect(escapeRegExp('a)b')).toBe('a\\)b');
+ expect(escapeRegExp('a|b')).toBe('a\\|b');
+ expect(escapeRegExp('a[b')).toBe('a\\[b');
+ expect(escapeRegExp('a]b')).toBe('a\\]b');
+ expect(escapeRegExp('a{b')).toBe('a\\{b');
+ expect(escapeRegExp('a}b')).toBe('a\\}b');
+ expect(escapeRegExp('a/b')).toBe('a\\/b');
+ expect(escapeRegExp('a\\b')).toBe('a\\\\b');
+ });
+});
diff --git a/src/utils/__tests__/generateValuesFromTemplate.test.ts b/src/utils/__tests__/generateValuesFromTemplate.test.ts
new file mode 100644
index 0000000..ba87263
--- /dev/null
+++ b/src/utils/__tests__/generateValuesFromTemplate.test.ts
@@ -0,0 +1,28 @@
+import { _soitCore } from '../../core';
+import { generateValuesFromTemplate } from '../generateValuesFromTemplate';
+
+describe('generateValuesFromTemplate', () => {
+ it('should generate values from a simple template', () => {
+ const template = ['a', 'b', 'c'] as any;
+ const values = generateValuesFromTemplate(template);
+ expect(values).toEqual(['abc']);
+ });
+ it('should generate values from a dynamic template', () => {
+ const template = [
+ _soitCore([1, 2, 3]),
+ _soitCore(['a', 'b', 'c']),
+ ] as const;
+ const values = generateValuesFromTemplate(template);
+ expect(values).toEqual([
+ '1a',
+ '1b',
+ '1c',
+ '2a',
+ '2b',
+ '2c',
+ '3a',
+ '3b',
+ '3c',
+ ]);
+ });
+});
diff --git a/src/utils/__tests__/getRegExpFromValues.test.ts b/src/utils/__tests__/getRegExpFromValues.test.ts
new file mode 100644
index 0000000..2ab1111
--- /dev/null
+++ b/src/utils/__tests__/getRegExpFromValues.test.ts
@@ -0,0 +1,30 @@
+import { _soitCore } from '../../core';
+import { getRegExpFromValues } from '../getRegExpFromValues';
+
+describe('getRegExpFromValues', () => {
+ it('should generate a regex from a simple template', () => {
+ const template = ['a', 'b', 'c'] as const;
+ const regExp = getRegExpFromValues(template);
+ expect(regExp.test('abc')).toBe(true);
+ expect(regExp.test('abcd')).toBe(false);
+ });
+ it('should generate a regex from a dynamic template', () => {
+ const template = [
+ _soitCore(['1', '2', '3']),
+ _soitCore(['a', 'b', 'c']),
+ ] as const;
+ const regExp = getRegExpFromValues(template);
+ expect(regExp.test('1a')).toBe(true);
+ expect(regExp.test('1b')).toBe(true);
+ expect(regExp.test('1c')).toBe(true);
+ expect(regExp.test('2a')).toBe(true);
+ expect(regExp.test('2b')).toBe(true);
+ expect(regExp.test('2c')).toBe(true);
+ expect(regExp.test('3a')).toBe(true);
+ expect(regExp.test('3b')).toBe(true);
+ expect(regExp.test('3c')).toBe(true);
+ expect(regExp.test('4a')).toBe(false);
+ expect(regExp.test('4b')).toBe(false);
+ expect(regExp.test('4c')).toBe(false);
+ });
+});
diff --git a/src/utils/__tests__/isSoit.test.ts b/src/utils/__tests__/isSoit.test.ts
new file mode 100644
index 0000000..a9174d3
--- /dev/null
+++ b/src/utils/__tests__/isSoit.test.ts
@@ -0,0 +1,17 @@
+import { SOIT_SYMBOL } from '../../constants';
+import { isSoit } from '../isSoit';
+
+describe('isSoit', () => {
+ it('should return true if the value is a Soit instance', () => {
+ const soit = () => {};
+ soit[SOIT_SYMBOL] = true;
+ expect(isSoit(soit as any)).toBe(true);
+ });
+
+ it('should return false if the value is not a Soit instance', () => {
+ expect(isSoit('a')).toBe(false);
+ expect(isSoit(0)).toBe(false);
+ expect(isSoit(true)).toBe(false);
+ expect(isSoit(false)).toBe(false);
+ });
+});
diff --git a/src/utils/__tests__/toCaptureGroup.test.ts b/src/utils/__tests__/toCaptureGroup.test.ts
new file mode 100644
index 0000000..125bcd3
--- /dev/null
+++ b/src/utils/__tests__/toCaptureGroup.test.ts
@@ -0,0 +1,9 @@
+import { _soitCore } from '../../core';
+import { toCaptureGroup } from '../toCaptureGroup';
+
+describe('toCaptureGroup', () => {
+ it('should return a string with a capture group', () => {
+ const value = _soitCore(['a', 'b', 'c']);
+ expect(toCaptureGroup(value)).toBe('(a|b|c)');
+ });
+});
diff --git a/src/utils/escapeRegExp.ts b/src/utils/escapeRegExp.ts
new file mode 100644
index 0000000..725ad3a
--- /dev/null
+++ b/src/utils/escapeRegExp.ts
@@ -0,0 +1,5 @@
+import { Literal } from '../types/core.types';
+
+export function escapeRegExp(literal: Literal) {
+ return String(literal).replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
+}
diff --git a/src/utils/generateValuesFromTemplate.ts b/src/utils/generateValuesFromTemplate.ts
new file mode 100644
index 0000000..01652bd
--- /dev/null
+++ b/src/utils/generateValuesFromTemplate.ts
@@ -0,0 +1,40 @@
+import { isSoit } from './isSoit';
+import {
+ TemplateValue,
+ TemplateValues,
+ ValuesFromTemplate,
+} from '../types/template.types';
+
+function hasRestTemplate(
+ template: readonly TemplateValue[]
+): template is TemplateValues {
+ return template.length > 0;
+}
+
+export function generateValuesFromTemplate(
+ template: T,
+ acc: string
+): string[];
+
+export function generateValuesFromTemplate(
+ template: T
+): ValuesFromTemplate[];
+
+export function generateValuesFromTemplate<
+ T extends TemplateValues,
+ A extends string,
+>(template: T, acc: string = ''): any[] {
+ const [t0, ...restTemplate] = template;
+ if (!hasRestTemplate(restTemplate)) {
+ if (isSoit(t0)) {
+ return [...t0].map(value => `${acc}${value}`);
+ }
+ return [`${acc}${t0}`];
+ }
+ if (isSoit(t0)) {
+ return [...t0].flatMap(value =>
+ generateValuesFromTemplate(restTemplate, `${acc}${value}`)
+ );
+ }
+ return generateValuesFromTemplate(restTemplate, `${acc}${t0}`);
+}
diff --git a/src/utils/getRegExpFromValues.ts b/src/utils/getRegExpFromValues.ts
new file mode 100644
index 0000000..61aed00
--- /dev/null
+++ b/src/utils/getRegExpFromValues.ts
@@ -0,0 +1,17 @@
+import { isSoit } from './isSoit';
+import { escapeRegExp } from './escapeRegExp';
+import { toCaptureGroup } from './toCaptureGroup';
+import { TemplateValues } from '../types/template.types';
+
+export function getRegExpFromValues(templateValues: TemplateValues) {
+ return new RegExp(
+ `^${templateValues
+ .map(value => {
+ if (isSoit(value)) {
+ return toCaptureGroup(value);
+ }
+ return escapeRegExp(value);
+ })
+ .join('')}$`
+ );
+}
diff --git a/src/utils/isSoit.ts b/src/utils/isSoit.ts
new file mode 100644
index 0000000..3152328
--- /dev/null
+++ b/src/utils/isSoit.ts
@@ -0,0 +1,6 @@
+import { SOIT_SYMBOL } from '../constants';
+import { Literal, Soit } from '../types/core.types';
+
+export function isSoit(value: Literal | Soit): value is Soit {
+ return typeof value === 'function' && value.hasOwnProperty(SOIT_SYMBOL);
+}
diff --git a/src/utils/toCaptureGroup.ts b/src/utils/toCaptureGroup.ts
new file mode 100644
index 0000000..f914141
--- /dev/null
+++ b/src/utils/toCaptureGroup.ts
@@ -0,0 +1,6 @@
+import { Soit } from '../types/core.types';
+import { escapeRegExp } from './escapeRegExp';
+
+export function toCaptureGroup(value: Soit): string {
+ return `(${value.map(escapeRegExp).join('|')})`;
+}
diff --git a/tsconfig.json b/tsconfig.json
index 76745ff..aa1eecc 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -64,5 +64,5 @@
"skipLibCheck": true
},
"include": ["src"],
- "exclude": ["node_modules", "**/*.test.ts"]
+ "exclude": ["node_modules", "**/*.test.ts", "**/*.test-types.ts"]
}