soit
v2.1.0
Published
Create a type guard from a list of literals.
Downloads
46
Maintainers
Readme
Soit (French for: either) is like an enhanced Set() function which provides type narrowing and template beta
utils.
Motivation
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.
The simplest way to declare all possible values is to write a union:
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
Another approach is to use the enum feature of TypeScript:
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 to this set of values or to get an 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 an array of literals to the Soit()
function.
import Soit from "soit";
const isHTTPMethod = Soit(["GET", "POST", "PUT", "DELETE"]);
A literal is a specific string, number or boolean.
const isOne = Soit(["1", "one", 1, true]);
You can infer the corresponding type using the Infer
generic provided by the lib.
import { Infer } from "soit";
type HTTPMethod = Infer<typeof isHTTPMethod>; // infers "GET" | "POST" | "PUT" | "DELETE"
Guard behavior
A Soit instance is intended to be used as a type guard:
function handleHTTPMethod(method: string) {
if(isHTTPMethod(method)) {
// method's value is "GET", "POST", "PUT" or "DELETE"
}
throw new Error("Unknown HTTP method.");
}
Iterable behavior
Because the Soit instance is iterable, you can access the corresponding array:
const HTTPMethodArray = Array.from(isHTTPMethod);
You may prefer this syntax:
const HTTPMethodArray = [...isHTTPMethod];
Array methods deprecated
A Soit instance gives access to two Array methods : map
and forEach
isHTTPMethod.forEach(method => console.log(method));
const lowerCaseHTTPMethodArray = isHTTPMethod.map(method => method.toLowerCase());
map
andforEach
are simple shortcuts, e.g. :[...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 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.
const isHTTPMutationMethod = isHTTPMethod.subset(["POST", "PUT", "DELETE"]);
This checks on build time that
"POST"
,"PUT"
and"DELETE"
do exist in theisHTTPMethod
instance.
.extend([])
You can extend an existing Soit instance using the extend
method.
const isHTTPMethod = isHTTPMutationMethod.extend(["GET"]);
.difference([])
You can create a new instance without the specified values using the difference
method.
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 mechanism, but with runtime utils. Let's take the following template literal type:
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:
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.
type TimeGetter = Infer<typeof isTimeGetter>;
Guard behavior
Like a Soit instance, a SoitTemplate is intended to be used as a type guard:
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.
const [unit] = isTimeGetter.capture("getSeconds"); // unit === "Seconds"
Iterable behavior and Array method
A SoitTemplate instance is iterable.
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
You can easily integrate Soit instances to your patterns using the P.when util :
import { P } from "ts-pattern";
const pattern = P.when(isHTTPMethod);
The inference will work as expected with TS-Pattern logic :
type Pattern = P.infer<typeof pattern>; // infers "GET" | "POST" | "PUT" | "DELETE"
Zod
You can integrate Soit instances to your Zod schemas using the custom util:
import * as z from "zod";
import { Infer } from "soit";
type HTTPMethod = Infer<typeof isHTTPMethod>;
const HTTPMethodSchema = z.custom<HTTPMethod>(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)
You are maybe trying to create a new Soit instance using a named array.
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.
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 :
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:
const isTimeGetter = Soit.Template('get', Soit(['Seconds', 'Minutes', 'Hours'] as const));