I am using io-ts to define my typescript types, in order to do runtime type validation and type declarations from a single source.
In this usecase I would like to define an interface with a string member and that member should validate against a regular expression, e.g. a version string. So valid input would look like this:
{version: '1.2.3'}
The mechanism to do this seems to be via branded types and I came up with this:
import { isRight } from 'fp-ts/Either';
import { brand, Branded, string, type, TypeOf } from 'io-ts';
interface VersionBrand {
readonly Version: unique symbol; // use `unique symbol` here to ensure uniqueness across modules / packages
}
export const TypeVersion = brand(
string, // a codec representing the type to be refined
(value: string): value is Branded<string, VersionBrand> =>
/^\d+\.\d+\.\d+$/.test(value), // a custom type guard using the build-in helper `Branded`
'Version' // the name must match the readonly field in the brand
);
export const TypeMyStruct = type({
version: TypeVersion,
});
export type Version = TypeOf<typeof TypeVersion>;
export type MyStruct = TypeOf<typeof TypeMyStruct>;
export function callFunction(data: MyStruct): boolean {
// type check
const validation = TypeMyStruct.decode(data);
return isRight(validation);
}
This works as expected for type validation inside my callFunction method but I cannot invoke the function using a regular object, i.e. the following does not compile:
callFunction({ version: '1.2.3' });
This is failing with Type 'string' is not assignable to type 'Branded<string, VersionBrand>'.
While the error message makes sense, because Version is a specialization of string I would like to allow a caller to invoke the function with any string and then do a runtime validation that would check against the regular expression. Still I'd like to have some typing information, so I would not like to define the input as any.
Ideally there would be a way to derive a version VersionForInput from Version that uses the primitive data types for the branded fields, so it would be equivalent to:
interface VersionForInput { version: string }
Of course I could declare it explicitly but this would mean to duplicate the type definition to some extend.
Question: Is there a way in io-ts to derive an unbranded version from a branded version of a type? Is the use of a branded type even the right choice for this usecase? The goal is to do extra validation on top of a primitive type (e.g. extra regexp checks for a string value).