I have implemented io-ts for defining my typescript types. This allows me to perform runtime type validation and generate type declarations from a single source.
In this particular scenario, I aim to create an interface with a string member that needs to pass a regular expression validation, such as a version string. A valid input would resemble the following:
{version: '1.2.3'}
To achieve this, branded types seem to be the way forward. Here is the approach I have taken:
import { isRight } from 'fp-ts/Either';
import { brand, Branded, string, type, TypeOf } from 'io-ts';
interface VersionBrand {
readonly Version: unique symbol;
}
export const TypeVersion = brand(
string,
(value: string): value is Branded<string, VersionBrand> =>
/^\d+\.\d+\.\d+$/.test(value),
'Version'
);
export const TypeMyStruct = type({
version: TypeVersion,
});
export type Version = TypeOf<typeof TypeVersion>;
export type MyStruct = TypeOf<typeof TypeMyStruct>;
export function callFunction(data: MyStruct): boolean {
const validation = TypeMyStruct.decode(data);
return isRight(validation);
}
Although this setup successfully validates types within the callFunction
method, attempting to call the function with a normal object results in a compilation error:
callFunction({ version: '1.2.3' });
The error message states that
Type 'string' is not assignable to type 'Branded<string, VersionBrand>'
.
While the error is logical due to Version
being a specialization of string
, I am looking for a way to enable callers to invoke the function with any string and then validate it against the regular expression at runtime without resorting to using any
. Is there a way to derive an unbranded version from the branded version of a type in io-ts? And is utilizing a branded type the correct approach for this situation where additional validation is required on top of a primitive type?