The purpose behind this piece of code is to provide an easy and efficient way to bind a primitive value to a static type. The value is not wrapped, which means nothing will change at runtime, the value will still be a primitive value. I later on identified two official issues related to the problem I'm aiming to solve :
/!\ On November 16 2023, something happened. I discovered zod .brand, making my code completely useless. Wait, that was already the case, wasn't it ?
The way it works is simple :
- You explicitely declare your new type, for example
Percent
, which purpose is to hold an integer between 0 and 100. I'll now refer to this kind of type as avalidation bounded type
, orVBT
- You create and link a validation function to the type
- You're good to go ! Start frenzy typing :)
See index.ts
for a complete example and a way to get started, and test_typing.ts
to make sure the typing behaviour is correct.
Each VBT has the following properties :
- It is only compatible with itself
- It is stackable
A variable of a specific VBT can only receive a value of the same VBT. For example :
type GmailEmail = StringType<"GmailEmail">;
type OutlookEmail = StringType<"OutlookEmail">;
let e: GmailEmail = "" as OutlookEmail;
// Type 'OutlookEmail' is not assignable to type 'GmailEmail'.
But, it is possible to assign a VBT value to a variable of the corresponding primitive type :
let e: string = "" as OutlookEmail; // Works
let p: string = 1 as Percent; // Type 'Percent' is not assignable to type 'string'.
You can use an intersection to force a value to be compatible with several VBT :
function workWithLongEmail(value: Email & LongString): void {
console.log(`The string '${value}' is an email and a long string at the same time !`);
}
const myString: string = "[email protected]";
if (isEmail(myString)) {
workWithEmail(myString); // Works
//workWithLongString(myString); // Argument of type 'Email' is not assignable to parameter of type 'LongString'.
//workWithLongEmail(myString); // Argument of type 'Email' is not assignable to parameter of type ...
if (isLongString(myString)) {
workWithLongString(myString); // Works
workWithLongEmail(myString); // Works
}
}
// First, let's create the validation function
function validateIsEmail() { ... }
// Then, let's create the type
type Email = StringType<"Email">;
// Finally, let's make the type helpers functions to handle this type
const emailHelpers = makeTypeHelpers<Email>({
validate: validateIsEmail,
errorMessage(value) {
return `'${value}' is not a valid email address`;
}
});
// Start using it
try {
const email: Email = emailHelpers.from("[email protected]");
console.log(email, typeof email); // '[email protected]' string
const notAnEmail: Email = emailHelpers.from("not an email");
} catch (e) {
console.log((e as Error).message); // 'not an email' is not a valid email address
}
// You can also try assert() and is() methods
emailHelpers.is(value);
emailHelpers.assert(value);
Since this side project should be seen as a proof of concept, there are many ways to improve it, assuming that there is a real use case. For example :
- Make the VBTs composable, to combine them and prevent code duplication
- Integrate basic built-in VBTs, that are the most common. An example could be
PositiveInteger
- Find a better name for VBT, since it's not clear. Maybe something like "tagged type" or "verified type" would be better
Thanks for reading me ! Don't hesitate to open an issue for any suggestion or question :)